├── .gitignore
├── LICENSE
├── README.md
├── docs
├── index.4aaee98c.js
├── index.4aaee98c.js.map
├── index.8c9e9738.css
├── index.8c9e9738.css.map
└── index.html
├── example
├── .npmignore
├── index.html
├── index.tsx
├── package.json
├── tsconfig.json
└── yarn.lock
├── generate-exports.js
├── package.json
├── src
├── AutoSave.ts
├── FieldAssistiveText.tsx
├── FieldErrorMessage.tsx
├── FieldLabel.tsx
├── FieldSkeleton.tsx
├── FieldWrapper.tsx
├── Fields
│ ├── Checkbox
│ │ ├── CheckboxComponent.tsx
│ │ └── index.tsx
│ ├── Color
│ │ ├── ColorComponent.tsx
│ │ ├── ColorSettings.ts
│ │ └── index.tsx
│ ├── ContentHeader
│ │ ├── ContentHeaderComponent.tsx
│ │ ├── ContentHeaderSettings.ts
│ │ └── index.tsx
│ ├── ContentImage
│ │ ├── ContentImageComponent.tsx
│ │ ├── ContentImageSettings.ts
│ │ └── index.tsx
│ ├── ContentParagraph
│ │ ├── ContentParagraphComponent.tsx
│ │ ├── ContentParagraphSettings.ts
│ │ └── index.tsx
│ ├── ContentSubHeader
│ │ ├── ContentSubHeaderComponent.tsx
│ │ ├── ContentSubHeaderSettings.ts
│ │ └── index.tsx
│ ├── Date
│ │ ├── DateComponent.tsx
│ │ └── index.tsx
│ ├── DateTime
│ │ ├── DateTimeComponent.tsx
│ │ └── index.tsx
│ ├── Hidden
│ │ ├── HiddenComponent.tsx
│ │ ├── HiddenSettings.ts
│ │ └── index.tsx
│ ├── List
│ │ ├── ListComponent.tsx
│ │ ├── ListItem.tsx
│ │ ├── ListSettings.ts
│ │ └── index.tsx
│ ├── MultiSelect
│ │ ├── MultiSelectComponent.tsx
│ │ ├── MultiSelectSettings.ts
│ │ └── index.tsx
│ ├── Paragraph
│ │ ├── ParagraphComponent.tsx
│ │ ├── ParagraphSettings.ts
│ │ └── index.tsx
│ ├── Radio
│ │ ├── RadioComponent.tsx
│ │ ├── RadioSettings.ts
│ │ └── index.tsx
│ ├── Score
│ │ ├── ScoreComponent.tsx
│ │ ├── ScoreSettings.ts
│ │ └── index.tsx
│ ├── ShortText
│ │ ├── ShortTextComponent.tsx
│ │ ├── ShortTextSettings.ts
│ │ ├── ShortTextValidation.ts
│ │ └── index.tsx
│ ├── SingleSelect
│ │ ├── SingleSelectComponent.tsx
│ │ ├── SingleSelectSettings.ts
│ │ └── index.tsx
│ ├── Slider
│ │ ├── SliderComponent.tsx
│ │ ├── SliderSettings.ts
│ │ └── index.tsx
│ └── index.ts
├── Form.tsx
├── FormDialog.tsx
├── FormFields.tsx
├── FormWithContext.tsx
├── ScrollableDialogContent.tsx
├── SubmitButton.tsx
├── SubmitError.tsx
├── constants
│ └── fields.ts
├── index.ts
├── types.ts
├── useFormSettings.ts
└── utils.ts
├── tsconfig.json
├── tsdx.config.js
├── types
└── react-element-scroll-hook.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 | .parcel-cache
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Sidney Alcantara
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @rowy/form-builder
2 |
3 | https://rowyio.github.io/form-builder/
4 |
--------------------------------------------------------------------------------
/docs/index.8c9e9738.css:
--------------------------------------------------------------------------------
1 | .rcp-light {
2 | --rcp-background: #ffffff;
3 | --rcp-input-text: #111111;
4 | --rcp-input-border: rgba(0, 0, 0, 0.1);
5 | --rcp-input-label: #717171;
6 | }
7 |
8 | .rcp-dark {
9 | --rcp-background: #181818;
10 | --rcp-input-text: #f3f3f3;
11 | --rcp-input-border: rgba(255, 255, 255, 0.1);
12 | --rcp-input-label: #999999;
13 | }
14 |
15 | .rcp {
16 | display: flex;
17 | flex-direction: column;
18 | align-items: center;
19 |
20 | background-color: var(--rcp-background);
21 | border-radius: 10px;
22 | }
23 |
24 | .rcp-body {
25 | display: flex;
26 | flex-direction: column;
27 | align-items: center;
28 | justify-content: center;
29 | gap: 20px;
30 | width: 100%;
31 |
32 | box-sizing: border-box;
33 |
34 | padding: 20px;
35 | }
36 |
37 | .rcp-saturation {
38 | position: relative;
39 |
40 | width: 100%;
41 | background-image: linear-gradient(transparent, black),
42 | linear-gradient(to right, white, transparent);
43 | border-radius: 10px 10px 0 0;
44 |
45 | user-select: none;
46 | }
47 |
48 | .rcp-saturation-cursor {
49 | position: absolute;
50 |
51 | width: 20px;
52 | height: 20px;
53 |
54 | border: 2px solid #ffffff;
55 | border-radius: 50%;
56 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.15);
57 | box-sizing: border-box;
58 |
59 | transform: translate(-10px, -10px);
60 | }
61 |
62 | .rcp-hue {
63 | position: relative;
64 |
65 | width: 100%;
66 | height: 12px;
67 |
68 | background-image: linear-gradient(
69 | to right,
70 | rgb(255, 0, 0),
71 | rgb(255, 255, 0),
72 | rgb(0, 255, 0),
73 | rgb(0, 255, 255),
74 | rgb(0, 0, 255),
75 | rgb(255, 0, 255),
76 | rgb(255, 0, 0)
77 | );
78 | border-radius: 10px;
79 |
80 | user-select: none;
81 | }
82 |
83 | .rcp-hue-cursor {
84 | position: absolute;
85 |
86 | width: 20px;
87 | height: 20px;
88 |
89 | border: 2px solid #ffffff;
90 | border-radius: 50%;
91 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px;
92 | box-sizing: border-box;
93 |
94 | transform: translate(-10px, -4px);
95 | }
96 |
97 | .rcp-alpha {
98 | position: relative;
99 |
100 | width: 100%;
101 | height: 12px;
102 |
103 | border-radius: 10px;
104 |
105 | user-select: none;
106 | }
107 |
108 | .rcp-alpha-cursor {
109 | position: absolute;
110 |
111 | width: 20px;
112 | height: 20px;
113 |
114 | border: 2px solid #ffffff;
115 | border-radius: 50%;
116 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px;
117 | box-sizing: border-box;
118 |
119 | transform: translate(-10px, -4px);
120 | }
121 |
122 | .rcp-fields {
123 | display: grid;
124 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
125 | gap: 10px;
126 |
127 | width: 100%;
128 | }
129 |
130 | .rcp-fields-element {
131 | display: flex;
132 | flex-direction: column;
133 | align-items: center;
134 | gap: 5px;
135 |
136 | width: 100%;
137 | }
138 |
139 | .hex-element {
140 | grid-row: 1;
141 | }
142 |
143 | .hex-element:nth-child(3n) {
144 | grid-column: 1 / -1;
145 | }
146 |
147 | .rcp-fields-element-input {
148 | width: 100%;
149 |
150 | font-size: 14px;
151 | font-weight: 600;
152 |
153 | color: var(--rcp-input-text);
154 | text-align: center;
155 |
156 | background: none;
157 | border: 2px solid;
158 | border-color: var(--rcp-input-border);
159 | border-radius: 5px;
160 | box-sizing: border-box;
161 |
162 | outline: none;
163 |
164 | padding: 10px;
165 | }
166 |
167 | .rcp-fields-element-label {
168 | font-size: 14px;
169 | font-weight: 600;
170 |
171 | color: var(--rcp-input-label);
172 | text-transform: uppercase;
173 | }
174 |
175 | /*# sourceMappingURL=index.8c9e9738.css.map */
176 |
--------------------------------------------------------------------------------
/docs/index.8c9e9738.css.map:
--------------------------------------------------------------------------------
1 | {"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["../node_modules/react-color-palette/lib/css/styles.css"],"sourcesContent":[".rcp-light {\n --rcp-background: #ffffff;\n --rcp-input-text: #111111;\n --rcp-input-border: rgba(0, 0, 0, 0.1);\n --rcp-input-label: #717171;\n}\n\n.rcp-dark {\n --rcp-background: #181818;\n --rcp-input-text: #f3f3f3;\n --rcp-input-border: rgba(255, 255, 255, 0.1);\n --rcp-input-label: #999999;\n}\n\n.rcp {\n display: flex;\n flex-direction: column;\n align-items: center;\n\n background-color: var(--rcp-background);\n border-radius: 10px;\n}\n\n.rcp-body {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 20px;\n width: 100%;\n\n box-sizing: border-box;\n\n padding: 20px;\n}\n\n.rcp-saturation {\n position: relative;\n\n width: 100%;\n background-image: linear-gradient(transparent, black), linear-gradient(to right, white, transparent);\n border-radius: 10px 10px 0 0;\n \n user-select: none;\n}\n\n.rcp-saturation-cursor {\n position: absolute;\n\n width: 20px;\n height: 20px;\n\n border: 2px solid #ffffff;\n border-radius: 50%;\n box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.15);\n box-sizing: border-box;\n\n transform: translate(-10px, -10px);\n}\n\n.rcp-hue {\n position: relative;\n\n width: 100%;\n height: 12px;\n \n background-image: linear-gradient(\n to right,\n rgb(255, 0, 0),\n rgb(255, 255, 0),\n rgb(0, 255, 0),\n rgb(0, 255, 255),\n rgb(0, 0, 255),\n rgb(255, 0, 255),\n rgb(255, 0, 0)\n );\n border-radius: 10px;\n\n user-select: none\n}\n\n.rcp-hue-cursor {\n position: absolute;\n\n width: 20px;\n height: 20px;\n\n border: 2px solid #ffffff;\n border-radius: 50%;\n box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px;\n box-sizing: border-box;\n\n transform: translate(-10px, -4px);\n}\n\n.rcp-alpha {\n position: relative;\n\n width: 100%;\n height: 12px;\n\n border-radius: 10px;\n\n user-select: none;\n}\n\n.rcp-alpha-cursor {\n position: absolute;\n\n width: 20px;\n height: 20px;\n\n border: 2px solid #ffffff;\n border-radius: 50%;\n box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px;\n box-sizing: border-box;\n\n transform: translate(-10px, -4px);\n}\n\n.rcp-fields {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 10px;\n\n width: 100%;\n}\n\n.rcp-fields-element {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n\n width: 100%;\n}\n\n.hex-element {\n grid-row: 1;\n}\n\n.hex-element:nth-child(3n) {\n grid-column: 1 / -1;\n}\n\n.rcp-fields-element-input {\n width: 100%;\n \n font-size: 14px;\n font-weight: 600;\n\n color: var(--rcp-input-text);\n text-align: center;\n\n background: none;\n border: 2px solid;\n border-color: var(--rcp-input-border);\n border-radius: 5px;\n box-sizing: border-box;\n \n outline: none;\n\n padding: 10px;\n}\n\n.rcp-fields-element-label {\n font-size: 14px;\n font-weight: 600;\n\n color: var(--rcp-input-label);\n text-transform: uppercase;\n}\n"],"names":[],"version":3,"file":"index.8c9e9738.css.map","sourceRoot":"/__parcel_source_root/"}
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Playground
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/example/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 |
4 | import createTheme from '@mui/material/styles/createTheme';
5 | import ThemeProvider from '@mui/material/styles/ThemeProvider';
6 | import CssBaseline from '@mui/material/CssBaseline';
7 | import Button from '@mui/material/Button';
8 | import FormDialog from '../src/FormDialog';
9 | import { FieldType } from '../src';
10 |
11 | import { DndProvider } from 'react-dnd';
12 | import { HTML5Backend } from 'react-dnd-html5-backend';
13 |
14 | const theme = createTheme({
15 | typography: { fontFamily: 'system-ui' },
16 | props: { MuiTextField: { variant: 'filled' } },
17 | });
18 |
19 | const fields = [
20 | {
21 | type: FieldType.contentHeader,
22 | label: 'Header',
23 | name: '_contentHeader_1',
24 | },
25 | {
26 | type: FieldType.contentParagraph,
27 | label: 'This is a link!',
28 | name: '_contentParagraph_0',
29 | },
30 | // {
31 | // type: FieldType.richText,
32 | // name: 'desc',
33 | // label: 'Description',
34 | // maxCharacters: 20,
35 | // },
36 | {
37 | type: FieldType.shortText,
38 | format: 'url',
39 | name: 'meetingLink',
40 | label: 'Meeting Link',
41 | assistiveText: `
42 |
43 | What is my meeting link?
44 |
45 |
48 |
51 |
52 | `,
53 | required: true,
54 | },
55 | {
56 | type: FieldType.shortText,
57 | format: 'url',
58 | name: 'link',
59 | label: 'Link',
60 | displayCondition: 'return values.email.length > 0',
61 | },
62 | {
63 | type: FieldType.date,
64 | name: 'date',
65 | label: 'Your birthday',
66 | },
67 | {
68 | type: FieldType.dateTime,
69 | name: 'dateTime',
70 | label: 'Book a time',
71 |
72 | // disable past date time
73 | minDateTime: new Date(),
74 | },
75 | {
76 | type: FieldType.shortText,
77 | format: 'email',
78 | name: 'email',
79 | label: 'Email',
80 | gridCols: 6,
81 | },
82 | {
83 | type: FieldType.shortText,
84 | format: 'emailWithName',
85 | name: 'emailWithName',
86 | label: 'Email with Name',
87 | gridCols: 6,
88 | },
89 | {
90 | type: FieldType.shortText,
91 | format: 'phone',
92 | name: 'phone',
93 | label: 'Phone Number',
94 | gridCols: 6,
95 | },
96 | {
97 | type: FieldType.shortText,
98 | format: 'twitter',
99 | name: 'twitter',
100 | label: 'Twitter',
101 | gridCols: 6,
102 | maxCharacters: 15,
103 | validation: {
104 | '0': { 0: 'notOneOf', 1: ['admin'], 2: 'Reserved username' },
105 | 1: ['min', 3, 'Must be at least 3 characters'],
106 | },
107 | },
108 | {
109 | type: FieldType.shortText,
110 | format: 'linkedin',
111 | name: 'linkedin.text',
112 | label: 'LinkedIn',
113 | },
114 | {
115 | type: FieldType.shortText,
116 | name: 'linkedin.number',
117 | format: 'number',
118 | label: 'Number',
119 | conditional: 'check',
120 | defaultValue: 1,
121 | },
122 | {
123 | type: FieldType.shortText,
124 | name: 'header',
125 | label: 'Unique page header',
126 | placeholder: 'Selected startups for...',
127 | maxCharacters: 100,
128 | },
129 | {
130 | type: FieldType.paragraph,
131 | name: 'textarea',
132 | label: 'Tell me more',
133 | },
134 | {
135 | type: FieldType.contentSubHeader,
136 | label: 'Sub-Header',
137 | name: '_contentSubHeader_1',
138 | },
139 | {
140 | type: FieldType.singleSelect,
141 | name: 'singleSelect',
142 | label: 'Who is the CEO?',
143 | options: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
144 | },
145 | {
146 | type: FieldType.multiSelect,
147 | name: 'multiSelect',
148 | label: 'Who are team members?',
149 | options: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
150 | },
151 | {
152 | type: FieldType.multiSelect,
153 | name: 'multiSelect single',
154 | label: 'Who is CEO of Facebook?',
155 | options: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
156 | multiple: false,
157 | },
158 | {
159 | type: FieldType.contentSubHeader,
160 | label: 'Sub-Header 2',
161 | name: '_contentSubHeader_2',
162 | },
163 | {
164 | type: FieldType.contentParagraph,
165 | label: 'Paragraph text',
166 | name: '_contentParagraph_1',
167 | },
168 | {
169 | type: FieldType.checkbox,
170 | name: 'checkbox',
171 | label: 'I am not a robot',
172 | },
173 | {
174 | type: FieldType.radio,
175 | name: 'radio',
176 | label: 'Highest education level?',
177 | options: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
178 | },
179 | {
180 | type: FieldType.slider,
181 | name: 'slider',
182 | label: 'Your age',
183 | },
184 | {
185 | type: FieldType.list,
186 | name: 'textMulti',
187 | label: 'Previous employers',
188 | },
189 | {
190 | type: FieldType.color,
191 | name: 'color',
192 | label: 'Preferred color for your shirt?',
193 | },
194 | {
195 | type: FieldType.score,
196 | name: 'score',
197 | label: 'How likely are you to recommend us to a friend or a colleague?',
198 | minLabel: 'Not at all likely',
199 | maxLabel: 'Likely',
200 | },
201 | {
202 | type: FieldType.hidden,
203 | name: 'hidden',
204 | defaultValue: 'PERSISTENT VALUE',
205 | disablePadding: true,
206 | },
207 | ];
208 |
209 | const additionalFields = [
210 | {
211 | type: FieldType.shortText,
212 | name: 'shortTextWithDefaultValue',
213 | label: 'Short Text with Default Value',
214 | defaultValue: 'DEFAULT VALUE',
215 | },
216 | {
217 | type: FieldType.checkbox,
218 | name: 'checkboxWithDefaultValue',
219 | label: 'Checkbox with Default Value',
220 | defaultValue: true,
221 | },
222 | ];
223 |
224 | const App = () => {
225 | const [values, setValues] = React.useState({
226 | number: 123,
227 | multiSelect: ['Option 2'],
228 | meetingLink: 'https://example.com',
229 | });
230 | const [showAdditionalFields, setShowAdditionalFields] = React.useState(false);
231 |
232 | return (
233 |
234 |
235 |
236 | {}}
239 | title="Form Dialog"
240 | fields={
241 | showAdditionalFields ? [...additionalFields, ...fields] : fields
242 | }
243 | values={values}
244 | onSubmit={(data) => {
245 | console.log('data', data);
246 | setValues(data);
247 | }}
248 | // customActions={
249 | // <>
250 | //
251 | // >
252 | // }
253 | formHeader={
254 |
262 | }
263 | UseFormProps={{ mode: 'onTouched' }}
264 | />
265 |
266 |
267 | );
268 | };
269 |
270 | ReactDOM.render(, document.getElementById('root'));
271 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "start:cacheless": "rm -rf .parcel-cache && yarn start",
9 | "build": "parcel index.html --dist-dir ../docs --public-url ./"
10 | },
11 | "dependencies": {
12 | "@types/hoist-non-react-statics": ">= 3.3.1",
13 | "@types/node": ">= 12",
14 | "react": "^17.0.2",
15 | "react-dnd": "^14.0.4",
16 | "react-dnd-html5-backend": "^14.0.2",
17 | "react-dom": "^17.0.0",
18 | "yup": "^0.32.9"
19 | },
20 | "alias": {
21 | "@mui/material": "../node_modules/@mui/material",
22 | "react": "../node_modules/react",
23 | "react-dom": "../node_modules/react-dom/profiling",
24 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
25 | },
26 | "devDependencies": {
27 | "@hookform/devtools": "^3.1.0",
28 | "@types/react": "^17.0.33",
29 | "@types/react-dom": "^17.0.8",
30 | "@types/yup": "^0.29.12",
31 | "parcel": "^2.0.0",
32 | "typescript": "^4.4.4"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "esnext",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["esnext", "dom"],
16 | "baseUrl": ".",
17 | "types": ["node"],
18 | "importHelpers": true,
19 | "declaration": true,
20 | "strict": true,
21 | "noImplicitReturns": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "esModuleInterop": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/generate-exports.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | process.chdir('./src');
4 |
5 | const output = [];
6 |
7 | function scanDir(dir) {
8 | const items = fs.readdirSync(dir);
9 |
10 | for (const item of items) {
11 | // This is the generated file
12 | if (item === 'index.ts' && dir === '.') continue;
13 |
14 | // Recursively scan this directory
15 | if (!item.includes('.')) scanDir(dir + '/' + item);
16 |
17 | // Export the file’s contents
18 | if (!item.includes('.ts')) continue;
19 |
20 | // Get component/file name
21 | let component = item.split('.')[0];
22 | const path = component === 'index' ? dir : `${dir}/${component}`;
23 | if (component === 'index') component = dir.split('/').pop();
24 |
25 | // Check if file has default export
26 | const file = fs.readFileSync(dir + '/' + item);
27 | // Check if not field index file
28 | if (file.indexOf('export default') > -1 && item.split('.')[0] !== 'index')
29 | output.push(`export { default as ${component} } from '${path}';`);
30 |
31 | output.push(`export * from '${path}';\n`);
32 | }
33 | }
34 |
35 | scanDir('.');
36 | fs.writeFileSync('index.ts', output.join('\n'));
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.8.0",
3 | "name": "@rowy/form-builder",
4 | "publishConfig": {
5 | "access": "public"
6 | },
7 | "author": "Sidney Alcantara",
8 | "license": "MIT",
9 | "main": "dist/index.js",
10 | "typings": "dist/index.d.ts",
11 | "files": [
12 | "dist",
13 | "src"
14 | ],
15 | "engines": {
16 | "node": ">=10"
17 | },
18 | "scripts": {
19 | "start": "tsdx watch",
20 | "build": "node generate-exports && git add src/index.ts && tsdx build",
21 | "prepublishOnly": "node generate-exports && git add src/index.ts && tsdx build",
22 | "lint": "tsdx lint",
23 | "prepare": "tsdx build",
24 | "storybook": "start-storybook -p 6006",
25 | "build-storybook": "build-storybook"
26 | },
27 | "peerDependencies": {
28 | "@mui/icons-material": "^5.4.1",
29 | "@mui/material": "^5.4.1",
30 | "@mui/x-date-pickers": "^5.0.9",
31 | "@rowy/multiselect": "^0.2.3",
32 | "react": "^17.0.2"
33 | },
34 | "husky": {
35 | "hooks": {
36 | "pre-commit": "pretty-quick --staged; tsdx lint"
37 | }
38 | },
39 | "prettier": {
40 | "printWidth": 80,
41 | "semi": true,
42 | "singleQuote": true,
43 | "trailingComma": "es5"
44 | },
45 | "module": "dist/form-builder.esm.js",
46 | "devDependencies": {
47 | "@babel/core": "^7.14.6",
48 | "@emotion/react": "^11.4.0",
49 | "@emotion/styled": "^11.3.0",
50 | "@mui/icons-material": "^5.4.1",
51 | "@mui/material": "^5.4.1",
52 | "@mui/x-date-pickers": "^5.0.9",
53 | "@rowy/multiselect": "^0.4.1",
54 | "@storybook/addon-actions": "^6.3.8",
55 | "@storybook/addon-docs": "^6.3.8",
56 | "@storybook/addon-info": "^5.3.21",
57 | "@storybook/addon-links": "^6.3.8",
58 | "@storybook/addons": "^6.3.8",
59 | "@storybook/react": "^6.3.8",
60 | "@types/dompurify": "^2.2.2",
61 | "@types/react": "^17.0.13",
62 | "@types/react-color": "^3.0.4",
63 | "@types/react-dom": "^17.0.8",
64 | "@types/yup": "^0.29.12",
65 | "babel-loader": "^8.2.2",
66 | "eslint-plugin-prettier": "^3",
67 | "husky": "^4.2.5",
68 | "react": "^17",
69 | "react-docgen-typescript-loader": "^3.7.2",
70 | "react-dom": "^17.0.2",
71 | "react-is": "^17.0.2",
72 | "ts-loader": "^9.2.3",
73 | "tsdx": "^0.14.1",
74 | "tslib": "^2.3.0",
75 | "tss-react": "3",
76 | "typescript": "^4.3.5"
77 | },
78 | "dependencies": {
79 | "@hookform/resolvers": "^2.6.0",
80 | "@types/lodash-es": "^4.17.6",
81 | "array-move": "^3.0.1",
82 | "date-fns": "^2.22.1",
83 | "dompurify": "^2.2.9",
84 | "lodash-es": "^4.17.21",
85 | "mdi-material-ui": "^7.2.0",
86 | "react-color-palette": "^6.2.0",
87 | "react-dnd": "^14.0.2",
88 | "react-dnd-html5-backend": "^14.0.0",
89 | "react-element-scroll-hook": "^1.1.0",
90 | "react-hook-form": "^7.10.0",
91 | "use-debounce": "^8.0.0",
92 | "yup": "^0.32.9"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/AutoSave.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { Control, useWatch, useFormState } from 'react-hook-form';
3 | import { useDebounce } from 'use-debounce';
4 | import _isEqual from 'lodash-es/isEqual';
5 | import _omitBy from 'lodash-es/omitBy';
6 | import _isUndefined from 'lodash-es/isUndefined';
7 | import { diffChanges } from './utils';
8 |
9 | import { IFormProps } from './Form';
10 |
11 | export interface IAutoSaveProps {
12 | control: Control;
13 | defaultValues: NonNullable;
14 | onSubmit: IFormProps['onSubmit'];
15 | }
16 |
17 | export default function AutoSave({
18 | control,
19 | defaultValues,
20 | onSubmit,
21 | }: IAutoSaveProps) {
22 | const values = useWatch({ control });
23 | const { errors } = useFormState({ control });
24 |
25 | const [debouncedValues] = useDebounce(values, 1000, {
26 | equalityFn: _isEqual,
27 | });
28 |
29 | useEffect(() => {
30 | // - Update only fields that changed
31 | // - Remove values with errors
32 | // - Remove undefined value to prevent Firestore crash
33 | const newValues = _omitBy(
34 | diffChanges(defaultValues, debouncedValues),
35 | (value, name) => _isUndefined(value) || name in errors
36 | );
37 |
38 | if (Object.keys(newValues).length > 0) {
39 | console.log('SUBMIT', newValues, errors);
40 | onSubmit(newValues);
41 | }
42 | }, [debouncedValues]);
43 |
44 | return null;
45 | }
46 |
--------------------------------------------------------------------------------
/src/FieldAssistiveText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DOMPurify from 'dompurify';
3 |
4 | import { FormHelperText, FormHelperTextProps } from '@mui/material';
5 |
6 | export interface IFieldAssistiveTextProps
7 | extends Omit {
8 | disabled: boolean;
9 | }
10 |
11 | export default function FieldAssistiveText({
12 | children,
13 | ...props
14 | }: IFieldAssistiveTextProps) {
15 | if (!children) return null;
16 |
17 | const sanitizedChildren =
18 | typeof children === 'string' ? DOMPurify.sanitize(children) : null;
19 |
20 | if (sanitizedChildren)
21 | return (
22 |
28 | );
29 |
30 | return (
31 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/FieldErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { FormHelperText, FormHelperTextProps } from '@mui/material';
4 |
5 | export default function FieldErrorMessage(props: FormHelperTextProps) {
6 | if (!props.children) return null;
7 |
8 | return (
9 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/FieldLabel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { InputLabel, InputLabelProps } from '@mui/material';
4 |
5 | export interface IFieldLabelProps
6 | extends Omit {
7 | error: boolean;
8 | disabled: boolean;
9 | required: boolean;
10 | }
11 |
12 | export default function FieldLabel(props: IFieldLabelProps) {
13 | return (
14 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/FieldSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Skeleton, SkeletonProps } from '@mui/material';
3 |
4 | export default function FieldSkeleton(props: SkeletonProps) {
5 | return (
6 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/FieldWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { Controller } from 'react-hook-form';
3 |
4 | import { Grid } from '@mui/material';
5 | import FieldSkeleton from './FieldSkeleton';
6 |
7 | import { getFieldProp } from './fields';
8 |
9 | import { IFormFieldsProps } from './FormFields';
10 | import { Field, CustomComponent } from './types';
11 | import { controllerRenderPropsStub } from './utils';
12 |
13 | export interface IFieldWrapperProps
14 | extends Field,
15 | Omit {
16 | index: number;
17 | disabledConditional?: boolean;
18 | }
19 |
20 | /**
21 | * Finds the corresponding component for the field type and wraps it with
22 | * Controller.
23 | */
24 | export default function FieldWrapper({
25 | control,
26 | name,
27 | label,
28 | type,
29 | customComponents,
30 | gridCols = 12,
31 | disablePadding,
32 | disablePaddingTop,
33 | disabledConditional,
34 | defaultValue: defaultValueProp,
35 | setOmittedFields,
36 | ...props
37 | }: IFieldWrapperProps) {
38 | if (!type) {
39 | console.error(`Invalid field type: ${type}`, props);
40 | return null;
41 | }
42 |
43 | let fieldComponent: CustomComponent;
44 | // Pass defaultValue into the Controller for conditionally displayed fields
45 | let defaultValue: any = defaultValueProp;
46 |
47 | // Try to get fieldComponent from customComponents list
48 | if (
49 | !!customComponents &&
50 | Object.keys(customComponents).length > 0 &&
51 | type in customComponents
52 | ) {
53 | fieldComponent = customComponents[type].component;
54 |
55 | if (defaultValue === undefined)
56 | defaultValue = customComponents[type].defaultValue;
57 | }
58 | // If not found in customComponents, try to get it from the built-in components
59 | else {
60 | fieldComponent = getFieldProp('component', type);
61 |
62 | if (defaultValue === undefined)
63 | defaultValue = getFieldProp('defaultValue', type);
64 |
65 | // If not found in either, don’t display anything
66 | if (!fieldComponent) {
67 | console.error(`No matching field component for \`${type}\``);
68 | return null;
69 | }
70 | }
71 |
72 | if (!name) return null;
73 |
74 | const gridProps =
75 | typeof gridCols === 'number' ||
76 | typeof gridCols === 'string' ||
77 | typeof gridCols === 'boolean'
78 | ? { xs: gridCols }
79 | : gridCols;
80 |
81 | const styleOverrides = disablePadding
82 | ? { padding: 0 }
83 | : disablePaddingTop
84 | ? { paddingTop: 0 }
85 | : {};
86 |
87 | // If it’s a content field, don’t wrap with Controller
88 | if (getFieldProp('group', type) === 'content')
89 | return (
90 |
97 | }>
98 | {React.createElement(fieldComponent, {
99 | ...props,
100 | // Stub Controller render props
101 | ...controllerRenderPropsStub,
102 | disabled: true,
103 | name: name!, // Fix TypeScript error
104 | label: label!, // Fix TypeScript error
105 | })}
106 |
107 |
108 | );
109 |
110 | // If it’s a conditional field and the user hasn’t ticked, make sure the
111 | // Controller doesn’t register the field and there is no value for this field
112 | if (disabledConditional)
113 | return (
114 |
121 | }>
122 | {React.createElement(fieldComponent, {
123 | ...props,
124 | // Stub Controller render props
125 | ...controllerRenderPropsStub,
126 | disabled: true,
127 | name: name!, // Fix TypeScript error
128 | label: label!, // Fix TypeScript error
129 | })}
130 |
131 |
132 | );
133 |
134 | return (
135 |
142 | }>
143 |
147 | React.createElement(fieldComponent, {
148 | ...props,
149 | ...renderProps,
150 | name: name!, // Fix TypeScript error
151 | label: label!, // Fix TypeScript error
152 | errorMessage: renderProps.fieldState.error?.message,
153 | })
154 | }
155 | defaultValue={defaultValue}
156 | />
157 |
158 |
159 | );
160 | }
161 |
--------------------------------------------------------------------------------
/src/Fields/Checkbox/CheckboxComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import { FormControlLabel, Checkbox, CheckboxProps } from '@mui/material';
5 |
6 | import FieldErrorMessage from '../../FieldErrorMessage';
7 | import FieldAssistiveText from '../../FieldAssistiveText';
8 |
9 | export interface ICheckboxComponentProps
10 | extends IFieldComponentProps,
11 | Omit<
12 | CheckboxProps,
13 | 'name' | 'onChange' | 'checked' | 'ref' | 'value' | 'onBlur'
14 | > {}
15 |
16 | export default function CheckboxComponent({
17 | field: { onChange, onBlur, value, ref },
18 | fieldState,
19 | formState,
20 |
21 | name,
22 | useFormMethods,
23 |
24 | label,
25 | errorMessage,
26 | assistiveText,
27 |
28 | required,
29 |
30 | ...props
31 | }: ICheckboxComponentProps) {
32 | return (
33 | {
39 | onChange(e.target.checked);
40 | onBlur();
41 | }}
42 | inputProps={
43 | {
44 | 'data-type': 'checkbox',
45 | 'data-label': label ?? '',
46 | } as any
47 | }
48 | sx={[
49 | {
50 | '.MuiFormControlLabel-root:not(.Mui-disabled):hover &': {
51 | bgcolor: 'action.hover',
52 | },
53 | },
54 | ...(Array.isArray(props.sx)
55 | ? props.sx
56 | : props.sx
57 | ? [props.sx]
58 | : []),
59 | ]}
60 | inputRef={ref}
61 | />
62 | }
63 | onBlur={onBlur}
64 | label={
65 | <>
66 | {label}
67 | {required && <> *>}
68 |
69 | {errorMessage}
70 |
71 | {assistiveText}
72 |
73 | >
74 | }
75 | sx={{ mr: 0, display: 'flex' }}
76 | />
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/Fields/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import CheckboxMarked from 'mdi-material-ui/CheckboxMarked';
6 |
7 | const Component = lazy(
8 | () =>
9 | import('./CheckboxComponent') /* webpackChunkName: FormBuilder-Checkbox */
10 | );
11 |
12 | export const CheckboxConfig: IFieldConfig = {
13 | type: FieldType.checkbox,
14 | name: 'Checkbox',
15 | group: 'input',
16 | icon: ,
17 | dataType: 'boolean',
18 | defaultValue: false,
19 | component: Component,
20 | settings: [],
21 | validation: (config: Record) => {
22 | const validation: any[][] = [['boolean']];
23 |
24 | if (config.required === true)
25 | validation.push(['oneOf', [true], `Please tick the box`]);
26 |
27 | return validation;
28 | },
29 | };
30 | export default CheckboxConfig;
31 |
--------------------------------------------------------------------------------
/src/Fields/Color/ColorComponent.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 | import { ColorPicker, toColor } from 'react-color-palette';
4 | import 'react-color-palette/lib/css/styles.css';
5 |
6 | import {
7 | TextField,
8 | TextFieldProps,
9 | InputAdornment,
10 | Box,
11 | IconButton,
12 | Popover,
13 | } from '@mui/material';
14 | import PaletteIcon from '@mui/icons-material/Palette';
15 |
16 | import FieldAssistiveText from '../../FieldAssistiveText';
17 |
18 | export interface IColorComponentProps extends IFieldComponentProps {
19 | enableAlpha?: boolean;
20 | TextFieldProps?: Partial;
21 | }
22 |
23 | export default function ColorComponent({
24 | field: { onChange, onBlur, value, ref },
25 |
26 | label,
27 | errorMessage,
28 | assistiveText,
29 |
30 | required,
31 | disabled,
32 |
33 | enableAlpha,
34 | TextFieldProps,
35 | }: IColorComponentProps) {
36 | const anchorEl = useRef(null);
37 | const [open, setOpen] = useState(false);
38 | const handleOpen = () => setOpen(true);
39 |
40 | return (
41 | <>
42 |
47 |
53 | `0 0 0 1px ${theme.palette.action.disabled} inset`,
54 | borderRadius: '50%',
55 |
56 | backgroundColor: value.hex,
57 | }}
58 | />
59 |
60 | ),
61 | endAdornment: (
62 |
63 |
69 |
70 |
71 |
72 | ),
73 | }}
74 | onClick={handleOpen}
75 | {...TextFieldProps}
76 | value={value?.hex}
77 | label={label}
78 | inputProps={{ readOnly: true, required: false }}
79 | error={!!errorMessage}
80 | helperText={
81 | (errorMessage || assistiveText) && (
82 | <>
83 | {errorMessage}
84 |
85 |
86 | {assistiveText}
87 |
88 | >
89 | )
90 | }
91 | required={required}
92 | disabled={disabled}
93 | ref={anchorEl}
94 | inputRef={ref}
95 | />
96 | {!disabled && anchorEl.current && (
97 | {
101 | setOpen(false);
102 | onBlur();
103 | }}
104 | PaperProps={{ 'data-type': 'color-picker' } as any}
105 | anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
106 | transformOrigin={{ vertical: 'top', horizontal: 'center' }}
107 | >
108 |
115 |
116 | )}
117 | >
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/src/Fields/Color/ColorSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ColorSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.checkbox,
7 | name: 'enableAlpha',
8 | label: 'Enable alpha channel (user can add semi-transparent colors)',
9 | defaultValue: false,
10 | },
11 | ];
12 |
13 | export default ColorSettings;
14 |
--------------------------------------------------------------------------------
/src/Fields/Color/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import Palette from 'mdi-material-ui/Palette';
6 |
7 | import Settings from './ColorSettings';
8 | const Component = lazy(
9 | () => import('./ColorComponent') /* webpackChunkName: FormBuilder-Color */
10 | );
11 |
12 | export const ColorConfig: IFieldConfig = {
13 | type: FieldType.color,
14 | name: 'Color',
15 | group: 'input',
16 | icon: ,
17 | dataType: 'Record',
18 | defaultValue: null,
19 | component: Component,
20 | settings: Settings,
21 | validation: () => [['object'], ['nullable']],
22 | };
23 | export default ColorConfig;
24 |
--------------------------------------------------------------------------------
/src/Fields/ContentHeader/ContentHeaderComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import { Box, Typography, TypographyProps, Divider } from '@mui/material';
5 |
6 | export interface IContentHeaderComponentProps
7 | extends IFieldComponentProps,
8 | Partial> {}
9 |
10 | export default function ContentHeaderComponent({
11 | field,
12 | fieldState,
13 | formState,
14 |
15 | index,
16 | label,
17 | children,
18 | className,
19 |
20 | disabled,
21 | errorMessage,
22 | name,
23 | useFormMethods,
24 | ...props
25 | }: IContentHeaderComponentProps) {
26 | return (
27 |
41 |
46 | {children ?? label}
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/Fields/ContentHeader/ContentHeaderSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ContentHeaderSettings: IFieldConfig['settings'] = [
5 | {
6 | name: 'label',
7 | label: 'Header',
8 | type: FieldType.shortText,
9 | required: true,
10 | defaultValue: '',
11 | },
12 | ];
13 |
14 | export default ContentHeaderSettings;
15 |
--------------------------------------------------------------------------------
/src/Fields/ContentHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import FormatHeader1 from 'mdi-material-ui/FormatHeader1';
6 |
7 | import Settings from './ContentHeaderSettings';
8 | const Component = lazy(
9 | () =>
10 | import(
11 | './ContentHeaderComponent'
12 | ) /* webpackChunkName: FormBuilder-ContentHeader */
13 | );
14 |
15 | export const ContentHeaderConfig: IFieldConfig = {
16 | type: FieldType.contentHeader,
17 | name: 'Header',
18 | group: 'content',
19 | icon: ,
20 | dataType: 'undefined',
21 | defaultValue: undefined,
22 | component: Component,
23 | settings: Settings,
24 | validation: () => [],
25 | };
26 | export default ContentHeaderConfig;
27 |
--------------------------------------------------------------------------------
/src/Fields/ContentImage/ContentImageComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | export interface IContentImageComponentProps
5 | extends IFieldComponentProps,
6 | Omit, 'src'> {
7 | src: string | { downloadURL: string }[];
8 | }
9 |
10 | export default function ContentImageComponent({
11 | field,
12 | fieldState,
13 | formState,
14 |
15 | index,
16 | label,
17 | children,
18 |
19 | disabled,
20 | errorMessage,
21 | name,
22 | useFormMethods,
23 |
24 | src,
25 | alt,
26 | ...props
27 | }: IContentImageComponentProps) {
28 | if (!src || (Array.isArray(src) && (src.length === 0 || !src[0].downloadURL)))
29 | return null;
30 |
31 | return (
32 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/Fields/ContentImage/ContentImageSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ContentImageSettings: IFieldConfig['settings'] = [
5 | {
6 | name: 'src',
7 | label: 'Image Source',
8 | type: 'image',
9 | required: true,
10 | defaultValue: undefined,
11 | },
12 | {
13 | name: 'alt',
14 | label: 'Alt Text',
15 | type: FieldType.shortText,
16 | required: true,
17 | defaultValue: '',
18 | assistiveText:
19 | 'Learn more about alt text',
20 | },
21 | ];
22 |
23 | export default ContentImageSettings;
24 |
--------------------------------------------------------------------------------
/src/Fields/ContentImage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import Image from 'mdi-material-ui/Image';
6 |
7 | import Settings from './ContentImageSettings';
8 | const Component = lazy(
9 | () =>
10 | import(
11 | './ContentImageComponent'
12 | ) /* webpackChunkName: FormBuilder-ContentImage */
13 | );
14 |
15 | export const ContentImageConfig: IFieldConfig = {
16 | type: FieldType.contentImage,
17 | name: 'Image',
18 | group: 'content',
19 | icon: ,
20 | dataType: 'undefined',
21 | defaultValue: undefined,
22 | component: Component as any,
23 | settings: Settings,
24 | validation: () => [],
25 | };
26 | export default ContentImageConfig;
27 |
--------------------------------------------------------------------------------
/src/Fields/ContentParagraph/ContentParagraphComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 | import DOMPurify from 'dompurify';
4 |
5 | import { Typography, TypographyProps } from '@mui/material';
6 |
7 | const rootStyles = { mb: -1.5, whiteSpace: 'pre-line', cursor: 'default' };
8 |
9 | export interface IContentParagraphComponentProps
10 | extends IFieldComponentProps,
11 | Partial> {}
12 |
13 | export default function ContentParagraphComponent({
14 | field,
15 | fieldState,
16 | formState,
17 |
18 | index,
19 | label,
20 | children,
21 | className,
22 |
23 | disabled,
24 | errorMessage,
25 | name,
26 | useFormMethods,
27 | ...props
28 | }: IContentParagraphComponentProps) {
29 | if (children)
30 | return (
31 |
40 | {children}
41 |
42 | );
43 |
44 | const renderedLabel =
45 | typeof label === 'string' ? DOMPurify.sanitize(label) : null;
46 |
47 | if (renderedLabel)
48 | return (
49 |
59 | );
60 |
61 | return (
62 |
71 | {label}
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/Fields/ContentParagraph/ContentParagraphSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ContentParagraphSettings: IFieldConfig['settings'] = [
5 | {
6 | name: 'label',
7 | label: 'Paragraph',
8 | type: FieldType.paragraph,
9 | required: true,
10 | defaultValue: '',
11 | },
12 | ];
13 |
14 | export default ContentParagraphSettings;
15 |
--------------------------------------------------------------------------------
/src/Fields/ContentParagraph/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import Text from 'mdi-material-ui/Text';
6 |
7 | import Settings from './ContentParagraphSettings';
8 | const Component = lazy(
9 | () =>
10 | import(
11 | './ContentParagraphComponent'
12 | ) /* webpackChunkName: FormBuilder-ContentParagraph */
13 | );
14 |
15 | export const ContentParagraphConfig: IFieldConfig = {
16 | type: FieldType.contentParagraph,
17 | name: 'Paragraph',
18 | group: 'content',
19 | icon: ,
20 | dataType: 'undefined',
21 | defaultValue: undefined,
22 | component: Component,
23 | settings: Settings,
24 | validation: () => [],
25 | };
26 | export default ContentParagraphConfig;
27 |
--------------------------------------------------------------------------------
/src/Fields/ContentSubHeader/ContentSubHeaderComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import { Typography, TypographyProps } from '@mui/material';
5 |
6 | export interface IContentSubHeaderComponentProps
7 | extends IFieldComponentProps,
8 | Partial> {}
9 |
10 | export default function ContentSubHeaderComponent({
11 | field,
12 | fieldState,
13 | formState,
14 |
15 | index,
16 | label,
17 | children,
18 | className,
19 |
20 | disabled,
21 | errorMessage,
22 | name,
23 | useFormMethods,
24 | ...props
25 | }: IContentSubHeaderComponentProps) {
26 | return (
27 |
38 | {children ?? label}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/Fields/ContentSubHeader/ContentSubHeaderSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ContentSubHeaderSettings: IFieldConfig['settings'] = [
5 | {
6 | name: 'label',
7 | label: 'Sub-Header',
8 | type: FieldType.shortText,
9 | required: true,
10 | defaultValue: '',
11 | },
12 | ];
13 |
14 | export default ContentSubHeaderSettings;
15 |
--------------------------------------------------------------------------------
/src/Fields/ContentSubHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import FormatHeader2 from 'mdi-material-ui/FormatHeader2';
6 |
7 | import Settings from './ContentSubHeaderSettings';
8 | const Component = lazy(
9 | () =>
10 | import(
11 | './ContentSubHeaderComponent'
12 | ) /* webpackChunkName: FormBuilder-ContentSubHeader */
13 | );
14 |
15 | export const ContentSubHeaderConfig: IFieldConfig = {
16 | type: FieldType.contentSubHeader,
17 | name: 'Sub-Header',
18 | group: 'content',
19 | icon: ,
20 | dataType: 'undefined',
21 | defaultValue: undefined,
22 | component: Component,
23 | settings: Settings,
24 | validation: () => [],
25 | };
26 | export default ContentSubHeaderConfig;
27 |
--------------------------------------------------------------------------------
/src/Fields/Date/DateComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import {
5 | LocalizationProvider,
6 | DatePicker,
7 | DatePickerProps,
8 | } from '@mui/x-date-pickers';
9 | import { TextField, TextFieldProps } from '@mui/material';
10 | import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
11 |
12 | import FieldAssistiveText from '../../FieldAssistiveText';
13 |
14 | export interface IDateComponentProps
15 | extends IFieldComponentProps,
16 | Omit<
17 | DatePickerProps,
18 | 'label' | 'name' | 'onChange' | 'value' | 'ref'
19 | > {
20 | TextFieldProps: TextFieldProps;
21 | }
22 |
23 | export default function DateComponent({
24 | field: { onChange, onBlur, value, ref },
25 | fieldState,
26 | formState,
27 |
28 | name,
29 | useFormMethods,
30 |
31 | errorMessage,
32 | assistiveText,
33 |
34 | TextFieldProps,
35 | ...props
36 | }: IDateComponentProps) {
37 | let transformedValue: any = null;
38 | if (value && 'toDate' in value) transformedValue = value.toDate();
39 | else if (value !== undefined) transformedValue = value;
40 |
41 | return (
42 |
43 | (
54 |
64 | {errorMessage}
65 |
66 |
70 | {assistiveText}
71 |
72 | >
73 | )
74 | }
75 | data-type="date"
76 | data-label={props.label ?? ''}
77 | inputProps={{
78 | ...props.inputProps,
79 | required: false,
80 | }}
81 | sx={{
82 | '& .MuiInputBase-input': { fontVariantNumeric: 'tabular-nums' },
83 | ...TextFieldProps?.sx,
84 | }}
85 | />
86 | )}
87 | />
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/src/Fields/Date/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import Calendar from 'mdi-material-ui/Calendar';
6 |
7 | const Component = lazy(
8 | () => import('./DateComponent') /* webpackChunkName: FormBuilder-Date */
9 | );
10 |
11 | export const DateConfig: IFieldConfig = {
12 | type: FieldType.date,
13 | name: 'Date',
14 | group: 'input',
15 | icon: ,
16 | dataType: 'Date | null',
17 | defaultValue: null,
18 | component: Component as any,
19 | settings: [],
20 | validation: () => [
21 | ['date'],
22 | ['typeError', 'Please enter a valid date'],
23 | ['nullable'],
24 | ],
25 | };
26 | export default DateConfig;
27 |
--------------------------------------------------------------------------------
/src/Fields/DateTime/DateTimeComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import {
5 | LocalizationProvider,
6 | DateTimePicker,
7 | DateTimePickerProps,
8 | } from '@mui/x-date-pickers';
9 | import { TextField, TextFieldProps } from '@mui/material';
10 | import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
11 | import AccessTimeIcon from '@mui/icons-material/AccessTime';
12 |
13 | import FieldAssistiveText from '../../FieldAssistiveText';
14 |
15 | export interface IDateTimeComponentProps
16 | extends IFieldComponentProps,
17 | Omit<
18 | DateTimePickerProps,
19 | 'label' | 'name' | 'onChange' | 'value' | 'ref'
20 | > {
21 | TextFieldProps: TextFieldProps;
22 | }
23 |
24 | export default function DateTimeComponent({
25 | field: { onChange, onBlur, value, ref },
26 | fieldState,
27 | formState,
28 |
29 | name,
30 | useFormMethods,
31 |
32 | errorMessage,
33 | assistiveText,
34 |
35 | TextFieldProps,
36 | ...props
37 | }: IDateTimeComponentProps) {
38 | let transformedValue: any = null;
39 | if (value && 'toDate' in value) transformedValue = value.toDate();
40 | else if (value !== undefined) transformedValue = value;
41 |
42 | return (
43 |
44 | (
56 |
66 | {errorMessage}
67 |
68 |
72 | {assistiveText}
73 |
74 | >
75 | )
76 | }
77 | data-type="date-time"
78 | data-label={props.label ?? ''}
79 | inputProps={{
80 | ...props.inputProps,
81 | required: false,
82 | }}
83 | sx={{
84 | '& .MuiInputBase-input': { fontVariantNumeric: 'tabular-nums' },
85 | ...TextFieldProps?.sx,
86 | }}
87 | />
88 | )}
89 | />
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/src/Fields/DateTime/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import CalendarClock from 'mdi-material-ui/CalendarClock';
6 |
7 | const Component = lazy(
8 | () =>
9 | import('./DateTimeComponent') /* webpackChunkName: FormBuilder-DateTime */
10 | );
11 |
12 | export const DateTimeConfig: IFieldConfig = {
13 | type: FieldType.dateTime,
14 | name: 'Time & Date',
15 | group: 'input',
16 | icon: ,
17 | dataType: 'Date | null',
18 | defaultValue: null,
19 | component: Component as any,
20 | settings: [],
21 | validation: () => [
22 | ['date'],
23 | ['typeError', 'Please enter a valid date'],
24 | ['nullable'],
25 | ],
26 | };
27 | export default DateTimeConfig;
28 |
--------------------------------------------------------------------------------
/src/Fields/Hidden/HiddenComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | export default function HiddenComponent({
5 | field: { value, ref },
6 | name,
7 | }: IFieldComponentProps) {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/Fields/Hidden/HiddenSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const HiddenSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.shortText,
7 | name: 'defaultValue',
8 | label: 'Persistent Value',
9 | required: true,
10 | defaultValue: '',
11 | assistiveText:
12 | 'This value will always be submitted as part of the form. It cannot be edited by the user.',
13 | },
14 | {
15 | type: FieldType.checkbox,
16 | name: 'disablePadding',
17 | label: 'Disable padding',
18 | defaultValue: true,
19 | required: true,
20 | },
21 | ];
22 |
23 | export default HiddenSettings;
24 |
--------------------------------------------------------------------------------
/src/Fields/Hidden/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import EyeOff from 'mdi-material-ui/EyeOff';
6 |
7 | import Settings from './HiddenSettings';
8 | const Component = lazy(
9 | () => import('./HiddenComponent') /* webpackChunkName: FormBuilder-Hidden */
10 | );
11 |
12 | export const HiddenConfig: IFieldConfig = {
13 | type: FieldType.hidden,
14 | name: 'Hidden',
15 | group: 'input',
16 | icon: ,
17 | dataType: 'string',
18 | defaultValue: '',
19 | component: Component,
20 | settings: Settings,
21 | validation: () => [],
22 | };
23 | export default HiddenConfig;
24 |
--------------------------------------------------------------------------------
/src/Fields/List/ListComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import arrayMove from 'array-move';
3 | import { IFieldComponentProps } from '../../types';
4 | import { DndProvider } from 'react-dnd';
5 | import { HTML5Backend } from 'react-dnd-html5-backend';
6 |
7 | import { FormControl, Button, ButtonProps } from '@mui/material';
8 | import AddCircleIcon from '@mui/icons-material/AddCircle';
9 |
10 | import ListItem from './ListItem';
11 |
12 | import FieldLabel from '../../FieldLabel';
13 | import FieldErrorMessage from '../../FieldErrorMessage';
14 | import FieldAssistiveText from '../../FieldAssistiveText';
15 |
16 | export interface IListComponentProps extends IFieldComponentProps {
17 | itemLabel?: string;
18 | placeholder?: string;
19 | addButtonProps?: Partial;
20 | }
21 |
22 | export default function ListComponent({
23 | field: { onChange, value: valueProp, ref },
24 |
25 | name,
26 | useFormMethods,
27 |
28 | label,
29 | errorMessage,
30 | assistiveText,
31 |
32 | required,
33 | disabled,
34 |
35 | itemLabel = 'Item',
36 | placeholder,
37 | addButtonProps = {},
38 | }: IListComponentProps) {
39 | const value: string[] = Array.isArray(valueProp) ? valueProp : [];
40 | const add = () => onChange([...value, '']);
41 |
42 | const edit = (index: number) => (item: string) => {
43 | const newValue = [...useFormMethods.getValues(name)];
44 | newValue[index] = item;
45 | onChange(newValue);
46 | };
47 |
48 | const swap = (fromIndex: number, toIndex: number) => {
49 | const newValue = arrayMove(
50 | useFormMethods.getValues(name),
51 | fromIndex,
52 | toIndex
53 | );
54 | onChange(newValue);
55 | };
56 |
57 | const remove = (index: number) => () => {
58 | const newValue = [...useFormMethods.getValues(name)];
59 | newValue.splice(index, 1);
60 | onChange(newValue);
61 | };
62 |
63 | return (
64 |
74 |
79 | {label}
80 |
81 |
82 |
83 | {value.map((item, index) => (
84 |
96 | ))}
97 |
98 |
99 |
100 |
105 | }
106 | color="secondary"
107 | {...addButtonProps}
108 | onClick={add}
109 | sx={[
110 | { m: 0, ml: -0.5 },
111 | ...(Array.isArray(addButtonProps.sx)
112 | ? addButtonProps.sx
113 | : addButtonProps.sx
114 | ? [addButtonProps.sx]
115 | : []),
116 | ]}
117 | disabled={disabled}
118 | >
119 | Add {itemLabel}
120 |
121 |
122 |
123 | {errorMessage}
124 |
125 | {assistiveText}
126 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/src/Fields/List/ListItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { useDrag, useDrop } from 'react-dnd';
3 |
4 | import { Grid, TextField, IconButton } from '@mui/material';
5 | import DragHandleIcon from '@mui/icons-material/DragHandle';
6 | import RemoveCircleIcon from '@mui/icons-material/RemoveCircle';
7 |
8 | export interface IListItemProps {
9 | name: string;
10 | index: number;
11 | item: string;
12 |
13 | edit: (item: string) => void;
14 | swap: (fromIndex: number, toIndex: number) => void;
15 | remove: () => void;
16 |
17 | itemLabel?: string;
18 | placeholder?: string;
19 | disabled?: boolean;
20 | }
21 |
22 | export const MemoizedListItem = memo(
23 | function ListItem({
24 | name,
25 | index,
26 | item,
27 |
28 | edit,
29 | swap,
30 | remove,
31 |
32 | itemLabel,
33 | placeholder,
34 | disabled,
35 | }: IListItemProps) {
36 | const [, drag, dragPreview] = useDrag(() => ({
37 | type: name,
38 | item: { index },
39 | collect: (monitor) => ({
40 | isDragging: !!monitor.isDragging(),
41 | }),
42 | }));
43 |
44 | const [{ isOver }, drop] = useDrop(
45 | () => ({
46 | accept: name,
47 | drop: ({ index: fromIndex }: any) => swap(fromIndex, index),
48 | collect: (monitor) => ({
49 | isOver: !!monitor.isOver(),
50 | }),
51 | }),
52 | [index]
53 | );
54 |
55 | return (
56 |
57 |
58 |
71 |
75 |
76 |
77 |
78 | edit(e.target.value)}
85 | disabled={disabled}
86 | helperText=" "
87 | />
88 |
89 |
90 |
91 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | },
105 | ({ item: prevItem, index: prevIndex }, { item, index }) =>
106 | prevItem === item && prevIndex === index
107 | );
108 |
109 | export default MemoizedListItem;
110 |
--------------------------------------------------------------------------------
/src/Fields/List/ListSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ListSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.shortText,
7 | name: 'placeholder',
8 | label: 'Placeholder',
9 | defaultValue: '',
10 | },
11 | {
12 | type: FieldType.shortText,
13 | name: 'itemLabel',
14 | label: 'Item Label',
15 | defaultValue: '',
16 | },
17 | ];
18 |
19 | export default ListSettings;
20 |
--------------------------------------------------------------------------------
/src/Fields/List/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 | import * as yup from 'yup';
5 |
6 | import FormatListNumbered from 'mdi-material-ui/FormatListNumbered';
7 |
8 | import Settings from './ListSettings';
9 | const Component = lazy(
10 | () => import('./ListComponent') /* webpackChunkName: FormBuilder-List */
11 | );
12 |
13 | export const ListConfig: IFieldConfig = {
14 | type: FieldType.list,
15 | name: 'List',
16 | group: 'input',
17 | icon: ,
18 | dataType: 'string[]',
19 | defaultValue: [],
20 | component: Component,
21 | settings: Settings,
22 | validation: (config: Record) => {
23 | const validation: any[][] = [
24 | ['array'],
25 | ['of', yup.string().trim()],
26 | ['ensure'],
27 | ['compact'],
28 | ];
29 |
30 | if (config.required === true)
31 | validation.push(['min', 1, `${config.label || config.name} is required`]);
32 |
33 | return validation;
34 | },
35 | };
36 | export default ListConfig;
37 |
--------------------------------------------------------------------------------
/src/Fields/MultiSelect/MultiSelectComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 | import MultiSelect, { MultiSelectProps } from '@rowy/multiselect';
4 |
5 | import FieldAssistiveText from '../../FieldAssistiveText';
6 |
7 | export interface IMultiSelectComponentProps
8 | extends IFieldComponentProps,
9 | Omit, 'value' | 'onChange' | 'options' | 'label'> {
10 | options: (string | { value: string; label: React.ReactNode })[];
11 | }
12 |
13 | export default function MultiSelectComponent({
14 | field: { onChange, onBlur, value, ref },
15 | fieldState,
16 | formState,
17 |
18 | name,
19 | useFormMethods,
20 |
21 | errorMessage,
22 | assistiveText,
23 |
24 | options = [],
25 | ...props
26 | }: IMultiSelectComponentProps) {
27 | return (
28 |
48 | {errorMessage}
49 |
50 |
54 | {assistiveText}
55 |
56 | >
57 | ),
58 | onBlur,
59 | 'data-type': 'multi-select',
60 | 'data-label': props.label ?? '',
61 | inputRef: ref,
62 | }}
63 | />
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/Fields/MultiSelect/MultiSelectSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const MultiSelectSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.list,
7 | name: 'options',
8 | label: 'Options',
9 | defaultValue: [],
10 | },
11 | {
12 | type: FieldType.checkbox,
13 | name: 'searchable',
14 | label: 'User can search options',
15 | defaultValue: true,
16 | },
17 | {
18 | type: FieldType.shortText,
19 | name: 'labelPlural',
20 | label: 'Plural Label (if searchable)',
21 | defaultValue: undefined,
22 | displayCondition: 'return values.searchable === true',
23 | },
24 | {
25 | type: FieldType.checkbox,
26 | name: 'freeText',
27 | label: 'User can add new options',
28 | defaultValue: false,
29 | },
30 | {
31 | type: FieldType.checkbox,
32 | name: 'clearable',
33 | label: 'User can clear selection',
34 | defaultValue: true,
35 | },
36 | {
37 | type: FieldType.checkbox,
38 | name: 'selectAll',
39 | label: 'User can select all options',
40 | defaultValue: true,
41 | },
42 | ];
43 |
44 | export default MultiSelectSettings;
45 |
--------------------------------------------------------------------------------
/src/Fields/MultiSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 | import * as yup from 'yup';
5 |
6 | import OrderBoolAscendingVariant from 'mdi-material-ui/OrderBoolAscendingVariant';
7 |
8 | import Settings from './MultiSelectSettings';
9 | const Component = lazy(
10 | () =>
11 | import(
12 | './MultiSelectComponent'
13 | ) /* webpackChunkName: FormBuilder-MultiSelect */
14 | );
15 |
16 | export const MultiSelectConfig: IFieldConfig = {
17 | type: FieldType.multiSelect,
18 | name: 'Multi Select',
19 | group: 'input',
20 | icon: ,
21 | dataType: 'string[]',
22 | defaultValue: [],
23 | component: Component as any,
24 | settings: Settings,
25 | validation: (config: Record) => {
26 | const validation: any[][] = [
27 | ['array'],
28 | ['of', yup.string().trim()],
29 | ['ensure'],
30 | ['compact'],
31 | ];
32 |
33 | if (config.required === true)
34 | validation.push(['min', 1, 'Please make at least one selection']);
35 |
36 | if (typeof config.max === 'number')
37 | validation.push([
38 | 'max',
39 | config.max,
40 | `Please make at most ${config.max} selections`,
41 | ]);
42 |
43 | return validation;
44 | },
45 | };
46 | export default MultiSelectConfig;
47 |
--------------------------------------------------------------------------------
/src/Fields/Paragraph/ParagraphComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import {
5 | TextField,
6 | FilledTextFieldProps,
7 | Grid,
8 | FormHelperText,
9 | } from '@mui/material';
10 |
11 | import FieldAssistiveText from '../../FieldAssistiveText';
12 |
13 | export interface IParagraphComponentProps
14 | extends IFieldComponentProps,
15 | Omit<
16 | FilledTextFieldProps,
17 | 'variant' | 'name' | 'label' | 'onChange' | 'onBlur' | 'value' | 'ref'
18 | > {
19 | maxCharacters?: number;
20 | }
21 |
22 | export default function ParagraphComponent({
23 | field: { onChange, onBlur, value, ref },
24 | fieldState,
25 | formState,
26 |
27 | name,
28 | useFormMethods,
29 |
30 | errorMessage,
31 | assistiveText,
32 |
33 | disabled,
34 |
35 | hiddenLabel = false,
36 | maxCharacters,
37 | ...props
38 | }: IParagraphComponentProps) {
39 | return (
40 |
50 |
51 | {errorMessage}
52 |
53 |
54 | {assistiveText}
55 |
56 |
57 |
58 | {maxCharacters && (
59 |
60 |
64 | maxCharacters
65 | }
66 | >
67 | {typeof value === 'string' ? value.length : 0}
68 | /
69 | {maxCharacters}
70 |
71 |
72 | )}
73 |
74 | )
75 | }
76 | name={name}
77 | id={`field-${name}`}
78 | multiline
79 | minRows={3}
80 | {...props}
81 | disabled={disabled}
82 | inputProps={{
83 | required: false,
84 | // https://github.com/react-hook-form/react-hook-form/issues/4485
85 | disabled: false,
86 | readOnly: disabled,
87 | style: disabled ? { cursor: 'default' } : undefined,
88 | maxLength: maxCharacters ?? undefined,
89 | // Required for form-filler
90 | 'data-type': 'textarea',
91 | 'data-label': props.label ?? '',
92 | }}
93 | InputLabelProps={props.placeholder ? { shrink: true } : undefined}
94 | inputRef={ref}
95 | />
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/Fields/Paragraph/ParagraphSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ParagraphSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.paragraph,
7 | name: 'placeholder',
8 | label: 'Placeholder',
9 | defaultValue: '',
10 | },
11 | {
12 | type: FieldType.shortText,
13 | name: 'maxCharacters',
14 | label: 'Max characters',
15 | conditional: 'check',
16 | defaultValue: undefined,
17 | format: 'number',
18 | },
19 | ];
20 |
21 | export default ParagraphSettings;
22 |
--------------------------------------------------------------------------------
/src/Fields/Paragraph/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import FormTextarea from 'mdi-material-ui/FormTextarea';
6 |
7 | import Settings from './ParagraphSettings';
8 | const Component = lazy(
9 | () =>
10 | import('./ParagraphComponent') /* webpackChunkName: FormBuilder-Paragraph */
11 | );
12 |
13 | export const ParagraphConfig: IFieldConfig = {
14 | type: FieldType.paragraph,
15 | name: 'Paragraph',
16 | group: 'input',
17 | icon: ,
18 | dataType: 'string',
19 | defaultValue: '',
20 | component: Component,
21 | settings: Settings,
22 | validation: (config) => {
23 | const validation: any[][] = [['string'], ['trim']];
24 |
25 | if (typeof config.maxCharacters === 'number')
26 | validation.push([
27 | 'max',
28 | config.maxCharacters,
29 | 'You have reached the character limit',
30 | ]);
31 |
32 | return validation;
33 | },
34 | };
35 | export default ParagraphConfig;
36 |
--------------------------------------------------------------------------------
/src/Fields/Radio/RadioComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import {
5 | FormControl,
6 | FormControlLabel,
7 | RadioGroup,
8 | RadioGroupProps,
9 | Radio,
10 | Divider,
11 | Box,
12 | } from '@mui/material';
13 |
14 | import FieldLabel from '../../FieldLabel';
15 | import FieldErrorMessage from '../../FieldErrorMessage';
16 | import FieldAssistiveText from '../../FieldAssistiveText';
17 |
18 | export interface IRadioComponentProps
19 | extends IFieldComponentProps,
20 | Omit {
21 | options: (string | { value: string; label: React.ReactNode })[];
22 | }
23 |
24 | export default function RadioComponent({
25 | field: { onChange, onBlur, value, ref },
26 | fieldState,
27 | formState,
28 |
29 | name,
30 | useFormMethods,
31 |
32 | label,
33 | errorMessage,
34 | assistiveText,
35 |
36 | required,
37 |
38 | options,
39 | ...props
40 | }: IRadioComponentProps) {
41 | return (
42 |
48 |
54 | {label}
55 |
56 |
57 |
58 | {options.map((item) => {
59 | let option: { label: React.ReactNode; value: string } = {
60 | label: '',
61 | value: '',
62 | };
63 | if (typeof item === 'object') option = item;
64 | if (typeof item === 'string') option = { label: item, value: item };
65 |
66 | return (
67 |
68 |
93 | }
94 | sx={{
95 | mx: 0,
96 | '& .MuiFormControlLabel-label': {
97 | py: 14 / 8,
98 | ml: 1,
99 | marginTop: '0 !important',
100 | },
101 | }}
102 | />
103 |
104 |
105 | );
106 | })}
107 |
108 |
109 |
110 | {errorMessage}
111 |
112 | {assistiveText}
113 |
114 |
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/src/Fields/Radio/RadioSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const RadioSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.list,
7 | name: 'options',
8 | label: 'Options',
9 | defaultValue: [],
10 | },
11 | ];
12 |
13 | export default RadioSettings;
14 |
--------------------------------------------------------------------------------
/src/Fields/Radio/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import RadioboxMarked from 'mdi-material-ui/RadioboxMarked';
6 |
7 | import Settings from './RadioSettings';
8 | const Component = lazy(
9 | () => import('./RadioComponent') /* webpackChunkName: FormBuilder-Radio */
10 | );
11 |
12 | export const RadioConfig: IFieldConfig = {
13 | type: FieldType.radio,
14 | name: 'Radio',
15 | group: 'input',
16 | icon: ,
17 | dataType: 'string',
18 | defaultValue: '',
19 | component: Component as any,
20 | settings: Settings,
21 | validation: () => [['string'], ['trim']],
22 | };
23 | export default RadioConfig;
24 |
--------------------------------------------------------------------------------
/src/Fields/Score/ScoreComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import { FormControl, Box, Typography } from '@mui/material';
5 | import { ToggleButtonGroup, ToggleButton } from '@mui/material';
6 |
7 | import FieldLabel from '../../FieldLabel';
8 | import FieldErrorMessage from '../../FieldErrorMessage';
9 | import FieldAssistiveText from '../../FieldAssistiveText';
10 |
11 | export interface IScoreComponentProps extends IFieldComponentProps {
12 | min?: number;
13 | max?: number;
14 | minLabel?: React.ReactNode;
15 | maxLabel?: React.ReactNode;
16 | step?: number;
17 | }
18 |
19 | export default function ScoreComponent({
20 | field: { onChange, value, ref },
21 |
22 | label,
23 | errorMessage,
24 | assistiveText,
25 |
26 | disabled,
27 | required,
28 |
29 | min = 0,
30 | max = 10,
31 | minLabel,
32 | maxLabel,
33 | step = 1,
34 | }: IScoreComponentProps) {
35 | const buttons: React.ReactNodeArray = [];
36 | for (let i = min; i <= max; i += step)
37 | buttons.push(
38 |
63 | {i}
64 |
65 | );
66 |
67 | return (
68 |
74 |
87 | {label}
88 |
89 |
90 |
101 | {minLabel && (
102 |
107 | {minLabel}
108 |
109 | )}
110 |
111 | {
114 | if (v !== null) onChange(v);
115 | }}
116 | exclusive
117 | aria-label="Score"
118 | sx={{
119 | gridRow: 2,
120 | gridColumn: '1 / -1',
121 |
122 | display: 'flex',
123 | justifyContent: 'center',
124 |
125 | m: { xs: -1, md: -0.5 },
126 | flexWrap: { xs: 'wrap', md: 'nowrap' },
127 | }}
128 | >
129 | {buttons}
130 |
131 |
132 | {maxLabel && (
133 |
139 | {maxLabel}
140 |
141 | )}
142 |
143 |
144 | {errorMessage}
145 |
146 | {assistiveText}
147 |
148 |
149 | );
150 | }
151 |
--------------------------------------------------------------------------------
/src/Fields/Score/ScoreSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ScoreSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.shortText,
7 | name: 'min',
8 | label: 'Minimum Value',
9 | defaultValue: 0,
10 | format: 'number',
11 | gridCols: 6,
12 | },
13 | {
14 | type: FieldType.shortText,
15 | name: 'max',
16 | label: 'Maximum Value',
17 | defaultValue: 10,
18 | format: 'number',
19 | gridCols: 6,
20 | },
21 | {
22 | type: FieldType.shortText,
23 | name: 'minLabel',
24 | label: 'Minimum Label',
25 | defaultValue: '',
26 | gridCols: 6,
27 | },
28 | {
29 | type: FieldType.shortText,
30 | name: 'maxLabel',
31 | label: 'Maximum Label',
32 | defaultValue: '',
33 | gridCols: 6,
34 | },
35 | {
36 | type: FieldType.shortText,
37 | name: 'step',
38 | label: 'Step Size',
39 | defaultValue: 1,
40 | format: 'number',
41 | gridCols: 6,
42 | },
43 | ];
44 |
45 | export default ScoreSettings;
46 |
--------------------------------------------------------------------------------
/src/Fields/Score/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import Numeric10Box from 'mdi-material-ui/Numeric10Box';
6 |
7 | import Settings from './ScoreSettings';
8 | const Component = lazy(
9 | () => import('./ScoreComponent') /* webpackChunkName: FormBuilder-Score */
10 | );
11 |
12 | export const ScoreConfig: IFieldConfig = {
13 | type: FieldType.score,
14 | name: 'Score',
15 | group: 'input',
16 | icon: ,
17 | dataType: 'number',
18 | defaultValue: undefined,
19 | component: Component,
20 | settings: Settings,
21 | validation: () => [['number']],
22 | };
23 | export default ScoreConfig;
24 |
--------------------------------------------------------------------------------
/src/Fields/ShortText/ShortTextComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import { TextField, TextFieldProps, Grid, FormHelperText } from '@mui/material';
5 |
6 | import FieldAssistiveText from '../../FieldAssistiveText';
7 |
8 | export interface IShortTextComponentProps
9 | extends IFieldComponentProps,
10 | Omit<
11 | TextFieldProps,
12 | 'variant' | 'name' | 'label' | 'onBlur' | 'onChange' | 'value' | 'ref'
13 | > {
14 | format?:
15 | | 'email'
16 | | 'emailWithName'
17 | | 'phone'
18 | | 'number'
19 | | 'url'
20 | | 'twitter'
21 | | 'linkedin';
22 | maxCharacters?: number;
23 | }
24 |
25 | export default function ShortTextComponent({
26 | field: { onChange, onBlur, value, ref },
27 | fieldState,
28 | formState,
29 |
30 | name,
31 | useFormMethods,
32 |
33 | errorMessage,
34 | assistiveText,
35 |
36 | disabled,
37 |
38 | format,
39 | hiddenLabel = false,
40 | maxCharacters,
41 | ...props
42 | }: IShortTextComponentProps) {
43 | let variantProps: any = {};
44 | switch (format) {
45 | case 'email':
46 | variantProps = {
47 | type: 'email',
48 | placeholder: 'mail@domain.com',
49 | autoComplete: 'email',
50 | };
51 | break;
52 |
53 | case 'emailWithName':
54 | variantProps = {
55 | type: 'email',
56 | placeholder: 'Name ',
57 | autoComplete: 'email',
58 | };
59 | break;
60 |
61 | case 'phone':
62 | variantProps = {
63 | type: 'tel',
64 | placeholder: '+1234567890',
65 | autoComplete: 'tel',
66 | };
67 | break;
68 |
69 | case 'number':
70 | variantProps = {
71 | placeholder: '1234567890',
72 | inputProps: {
73 | inputMode: 'numeric',
74 | pattern: '\\d*',
75 | },
76 | };
77 | break;
78 |
79 | case 'url':
80 | variantProps = {
81 | type: 'url',
82 | placeholder: 'https://example.com',
83 | autoComplete: 'url',
84 | };
85 | break;
86 |
87 | case 'twitter':
88 | variantProps = { placeholder: '@username' };
89 | break;
90 |
91 | case 'linkedin':
92 | variantProps = {
93 | type: 'url',
94 | placeholder: 'https://linkedin.com/in/your-name',
95 | autoComplete: 'url',
96 | };
97 | break;
98 |
99 | default:
100 | break;
101 | }
102 |
103 | const hiddenLabelOverrideProps = hiddenLabel
104 | ? { label: '', 'aria-label': props.label as string, hiddenLabel: true }
105 | : {};
106 |
107 | const handleChange: TextFieldProps['onChange'] = (e) =>
108 | format === 'number'
109 | ? onChange(e.target.value === '' ? '' : Number(e.target.value))
110 | : onChange(e.target.value);
111 |
112 | return (
113 |
123 |
124 | {errorMessage}
125 |
126 |
127 | {assistiveText}
128 |
129 |
130 |
131 | {maxCharacters && (
132 |
133 |
137 | maxCharacters
138 | }
139 | >
140 | {typeof value === 'string' ? value.length : 0}
141 | /
142 | {maxCharacters}
143 |
144 |
145 | )}
146 |
147 | )
148 | }
149 | name={name}
150 | id={`field-${name}`}
151 | {...variantProps}
152 | {...props}
153 | {...hiddenLabelOverrideProps}
154 | disabled={disabled}
155 | inputProps={{
156 | required: false,
157 | // https://github.com/react-hook-form/react-hook-form/issues/4485
158 | disabled: false,
159 | readOnly: disabled,
160 | style: disabled ? { cursor: 'default' } : undefined,
161 | maxLength: maxCharacters ?? undefined,
162 | ...variantProps.inputProps,
163 | // Required for form-filler
164 | 'data-type': 'text',
165 | 'data-label': props.label ?? '',
166 | }}
167 | inputRef={ref}
168 | />
169 | );
170 | }
171 |
--------------------------------------------------------------------------------
/src/Fields/ShortText/ShortTextSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ShortTextSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.shortText,
7 | name: 'placeholder',
8 | label: 'Placeholder',
9 | defaultValue: '',
10 | },
11 | {
12 | type: FieldType.shortText,
13 | name: 'maxCharacters',
14 | label: 'Max characters',
15 | conditional: 'check',
16 | defaultValue: undefined,
17 | format: 'number',
18 | },
19 | {
20 | type: FieldType.singleSelect,
21 | name: 'format',
22 | label: 'Format',
23 | defaultValue: '',
24 | options: [
25 | { value: '', label: 'None' },
26 | { value: 'email', label: 'Email' },
27 | { value: 'phone', label: 'Phone' },
28 | { value: 'number', label: 'Number' },
29 | { value: 'url', label: 'URL' },
30 | { value: 'twitter', label: 'Twitter handle' },
31 | { value: 'linkedin', label: 'LinkedIn URL' },
32 | ],
33 | },
34 | {
35 | type: FieldType.singleSelect,
36 | name: 'autoComplete',
37 | label: 'Autocomplete Suggestion',
38 | defaultValue: '',
39 | options: [
40 | { value: '', label: 'None' },
41 | { value: 'name', label: 'Full Name' },
42 | { value: 'given-name', label: 'Given Name' },
43 | { value: 'family-name', label: 'Family Name' },
44 | { value: 'additional-name', label: 'Middle Name' },
45 | { value: 'email', label: 'Email' },
46 | { value: 'organization', label: 'Organisation' },
47 | { value: 'organization-title', label: 'Organisation title' },
48 | { value: 'street-address', label: 'Street address' },
49 | { value: 'country-name', label: 'Country name' },
50 | { value: 'bday', label: 'Birthday' },
51 | { value: 'tel', label: 'Phone number' },
52 | { value: 'url', label: 'URL' },
53 | ],
54 | assistiveText:
55 | 'Phones will suggest this value when the user clicks on this field. See all available values',
56 | freeText: true,
57 | },
58 | ];
59 |
60 | export default ShortTextSettings;
61 |
--------------------------------------------------------------------------------
/src/Fields/ShortText/ShortTextValidation.ts:
--------------------------------------------------------------------------------
1 | export const ShortTextValidation = (config: Record) => {
2 | const validation: any[][] = [['string'], ['trim']];
3 |
4 | switch (config.format) {
5 | case 'email':
6 | validation.push([
7 | 'email',
8 | 'Please enter the email in the format: mail@domain.com',
9 | ]);
10 | break;
11 |
12 | case 'emailWithName':
13 | validation.push([
14 | 'matches',
15 | /(?:"?([^"]*)"?\s)?(?:(.+@[^>]+)>?)/, // https://stackoverflow.com/a/14011481
16 | {
17 | message:
18 | 'Please enter the email in the format: Name ',
19 | excludeEmptyString: true,
20 | },
21 | ]);
22 | break;
23 |
24 | case 'phone':
25 | validation.push([
26 | 'matches',
27 | /^(?=(?:\D*\d\D*){8,14}$)[- \d()+]*/, // https://stackoverflow.com/a/28228199
28 | {
29 | message: 'Please enter a valid phone number',
30 | excludeEmptyString: true,
31 | },
32 | ]);
33 | break;
34 |
35 | case 'number':
36 | validation[0] = ['number'];
37 | // https://github.com/jquense/yup/issues/298#issuecomment-559017330
38 | validation.push([
39 | 'transform',
40 | (value: any) => {
41 | if ((typeof value === 'string' && value === '') || isNaN(value))
42 | return null;
43 | return value;
44 | },
45 | ]);
46 | validation.push(['nullable']);
47 | break;
48 |
49 | case 'url':
50 | validation.push([
51 | 'url',
52 | 'Please enter the URL in the format: https://example.com',
53 | ]);
54 | break;
55 |
56 | case 'twitter':
57 | validation.push([
58 | 'matches',
59 | /^@?(\w){1,15}$/, //https://stackoverflow.com/a/8650024
60 | {
61 | message: 'Please enter the Twitter account in the format: @username',
62 | excludeEmptyString: true,
63 | },
64 | ]);
65 | break;
66 |
67 | case 'linkedin':
68 | validation.push([
69 | 'matches',
70 | /^https?:\/\/([a-z]+.)?linkedin\.com\/in\/[a-zA-z\d-]+/,
71 | {
72 | message:
73 | 'Please enter the LinkedIn URL in the format: https://linkedin.com/in/your-name',
74 | excludeEmptyString: true,
75 | },
76 | ]);
77 | break;
78 |
79 | default:
80 | break;
81 | }
82 |
83 | if (typeof config.maxCharacters === 'number')
84 | validation.push([
85 | 'max',
86 | config.maxCharacters,
87 | 'You have reached the character limit',
88 | ]);
89 |
90 | return validation;
91 | };
92 |
93 | export default ShortTextValidation;
94 |
--------------------------------------------------------------------------------
/src/Fields/ShortText/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import FormTextbox from 'mdi-material-ui/FormTextbox';
6 |
7 | import Settings from './ShortTextSettings';
8 | import validation from './ShortTextValidation';
9 | const Component = lazy(
10 | () =>
11 | import('./ShortTextComponent') /* webpackChunkName: FormBuilder-ShortText */
12 | );
13 |
14 | export const ShortTextConfig: IFieldConfig = {
15 | type: FieldType.shortText,
16 | name: 'Short Text',
17 | group: 'input',
18 | icon: ,
19 | dataType: 'string',
20 | defaultValue: '',
21 | component: Component,
22 | settings: Settings,
23 | validation,
24 | };
25 | export default ShortTextConfig;
26 |
--------------------------------------------------------------------------------
/src/Fields/SingleSelect/SingleSelectComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 | import MultiSelect, { MultiSelectProps } from '@rowy/multiselect';
4 |
5 | import { TextField, FilledTextFieldProps, MenuItem } from '@mui/material';
6 |
7 | import FieldAssistiveText from '../../FieldAssistiveText';
8 |
9 | export interface ISingleSelectComponentProps
10 | extends IFieldComponentProps,
11 | Omit<
12 | FilledTextFieldProps,
13 | 'variant' | 'label' | 'name' | 'onBlur' | 'onChange' | 'ref' | 'value'
14 | >,
15 | Partial<
16 | Omit, 'value' | 'onChange' | 'options' | 'label'>
17 | > {
18 | options: (string | { value: string; label: React.ReactNode })[];
19 | }
20 |
21 | export default function SingleSelectComponent({
22 | field: { onChange, onBlur, value, ref },
23 | fieldState,
24 | formState,
25 |
26 | name,
27 | useFormMethods,
28 |
29 | errorMessage,
30 | assistiveText,
31 |
32 | options = [],
33 | ...props
34 | }: ISingleSelectComponentProps) {
35 | const sanitisedValue = (Array.isArray(value) ? value[0] : value) ?? '';
36 |
37 | // Render MultiSelect if one of the following props is defined
38 | if (
39 | [
40 | props.searchable,
41 | props.labelPlural,
42 | props.freeText,
43 | props.clearable,
44 | ].reduce((a, c) => a || c !== undefined, false)
45 | )
46 | return (
47 | onChange(value ?? '')}
53 | onBlur={onBlur}
54 | TextFieldProps={{
55 | ...props.TextFieldProps,
56 | error: !!errorMessage,
57 | InputLabelProps: {
58 | required: props.required,
59 | ...props.TextFieldProps?.InputLabelProps,
60 | },
61 | FormHelperTextProps: {
62 | component: 'div',
63 | ...props.TextFieldProps?.FormHelperTextProps,
64 | },
65 | helperText: (errorMessage || assistiveText) && (
66 | <>
67 | {errorMessage}
68 |
69 |
73 | {assistiveText}
74 |
75 | >
76 | ),
77 | onBlur,
78 | 'data-type': 'multi-select-single',
79 | 'data-label': props.label ?? '',
80 | inputRef: ref,
81 | }}
82 | clearable={props.clearable === true}
83 | />
84 | );
85 |
86 | // Render basic Material-UI select
87 | return (
88 |
96 | {errorMessage}
97 |
98 |
102 | {assistiveText}
103 |
104 | >
105 | )
106 | }
107 | {...props}
108 | onChange={onChange}
109 | onBlur={onBlur}
110 | // Convert string[] value to string
111 | // And remove MUI error when `undefined` or `null` is passed
112 | value={(Array.isArray(value) ? value[0] : value) ?? ''}
113 | data-label={props.label ?? ''}
114 | data-type={'single-select'}
115 | inputProps={{ required: false, ...props.inputProps }}
116 | inputRef={ref}
117 | >
118 | {options.map((option) => {
119 | if (typeof option === 'object')
120 | return (
121 |
124 | );
125 | return (
126 |
129 | );
130 | })}
131 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/src/Fields/SingleSelect/SingleSelectSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const SingleSelectSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.list,
7 | name: 'options',
8 | label: 'Options',
9 | defaultValue: [],
10 | },
11 | {
12 | type: FieldType.checkbox,
13 | name: 'searchable',
14 | label: 'User can search options',
15 | defaultValue: undefined,
16 | },
17 | {
18 | type: FieldType.shortText,
19 | name: 'labelPlural',
20 | label: 'Plural Label',
21 | defaultValue: undefined,
22 | displayCondition: 'return values.searchable === true',
23 | },
24 | {
25 | type: FieldType.checkbox,
26 | name: 'freeText',
27 | label: 'User can add new options',
28 | defaultValue: undefined,
29 | },
30 | {
31 | type: FieldType.checkbox,
32 | name: 'clearable',
33 | label: 'User can clear selection',
34 | defaultValue: undefined,
35 | },
36 | ];
37 |
38 | export default SingleSelectSettings;
39 |
--------------------------------------------------------------------------------
/src/Fields/SingleSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import OrderBoolDescending from 'mdi-material-ui/OrderBoolDescending';
6 |
7 | import Settings from './SingleSelectSettings';
8 | const Component = lazy(
9 | () =>
10 | import(
11 | './SingleSelectComponent'
12 | ) /* webpackChunkName: FormBuilder-SingleSelect */
13 | );
14 |
15 | export const SingleSelectConfig: IFieldConfig = {
16 | type: FieldType.singleSelect,
17 | name: 'Single Select',
18 | group: 'input',
19 | icon: ,
20 | dataType: 'string',
21 | defaultValue: '',
22 | component: Component as any,
23 | settings: Settings,
24 | validation: () => [['string'], ['trim']],
25 | };
26 | export default SingleSelectConfig;
27 |
--------------------------------------------------------------------------------
/src/Fields/Slider/SliderComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import {
5 | FormControl,
6 | Stack,
7 | Slider,
8 | SliderProps,
9 | Typography,
10 | } from '@mui/material';
11 |
12 | import FieldLabel from '../../FieldLabel';
13 | import FieldErrorMessage from '../../FieldErrorMessage';
14 | import FieldAssistiveText from '../../FieldAssistiveText';
15 |
16 | export interface ISliderComponentProps
17 | extends IFieldComponentProps,
18 | Omit {
19 | units?: string;
20 | unitsPlural?: string;
21 | minLabel?: React.ReactNode;
22 | maxLabel?: React.ReactNode;
23 | }
24 |
25 | const valueWithUnits = (value: number, units?: string, unitsPlural?: string) =>
26 | `${value} ${(value !== 1 ? unitsPlural || '' : units) || ''}`.trim();
27 |
28 | export default function SliderComponent({
29 | field: { onChange, onBlur, value, ref },
30 | fieldState,
31 | formState,
32 |
33 | name,
34 | useFormMethods,
35 |
36 | label,
37 | errorMessage,
38 | assistiveText,
39 |
40 | required,
41 |
42 | units,
43 | unitsPlural,
44 | minLabel,
45 | maxLabel,
46 | min = 0,
47 | max = 100,
48 | ...props
49 | }: ISliderComponentProps) {
50 | const handleChange = (_: any, value: number | number[]) => {
51 | onChange(value);
52 | onBlur();
53 | };
54 |
55 | const getAriaValueText = (value: number) =>
56 | valueWithUnits(value, units, unitsPlural);
57 | const getValueLabelFormat = getAriaValueText;
58 |
59 | return (
60 |
66 |
71 | {label}
72 |
73 |
74 |
75 |
76 | {minLabel || valueWithUnits(min, units, unitsPlural)}
77 |
78 |
79 |
93 |
94 |
95 | {maxLabel || valueWithUnits(max, units, unitsPlural)}
96 |
97 |
98 |
99 | {errorMessage}
100 |
101 | {assistiveText}
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/src/Fields/Slider/SliderSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const SliderSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.shortText,
7 | name: 'min',
8 | label: 'Minimum value',
9 | required: true,
10 | defaultValue: 0,
11 | gridCols: 6,
12 | format: 'number',
13 | },
14 | {
15 | type: FieldType.shortText,
16 | name: 'minLabel',
17 | label: 'Minimum label',
18 | defaultValue: '',
19 | gridCols: 6,
20 | },
21 | {
22 | type: FieldType.shortText,
23 | name: 'max',
24 | label: 'Maximum value',
25 | required: true,
26 | defaultValue: 0,
27 | gridCols: 6,
28 | format: 'number',
29 | },
30 | {
31 | type: FieldType.shortText,
32 | name: 'maxLabel',
33 | label: 'Maximum label',
34 | defaultValue: '',
35 | gridCols: 6,
36 | },
37 | {
38 | type: FieldType.shortText,
39 | name: 'units',
40 | label: 'Units',
41 | defaultValue: '',
42 | gridCols: 6,
43 | placeholder: 'year',
44 | },
45 | {
46 | type: FieldType.shortText,
47 | name: 'unitsPlural',
48 | label: 'Plural Units',
49 | defaultValue: '',
50 | gridCols: 6,
51 | placeholder: 'years',
52 | },
53 | {
54 | type: FieldType.shortText,
55 | name: 'step',
56 | label: 'Step Size',
57 | required: true,
58 | defaultValue: 1,
59 | gridCols: 6,
60 | format: 'number',
61 | },
62 | ];
63 |
64 | export default SliderSettings;
65 |
--------------------------------------------------------------------------------
/src/Fields/Slider/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { lazy } from 'react';
2 | import { IFieldConfig } from '../../types';
3 | import { FieldType } from '../../constants/fields';
4 |
5 | import GestureSwipeHorizontal from 'mdi-material-ui/GestureSwipeHorizontal';
6 |
7 | import Settings from './SliderSettings';
8 | const Component = lazy(
9 | () => import('./SliderComponent') /* webpackChunkName: FormBuilder-Slider */
10 | );
11 |
12 | export const SliderConfig: IFieldConfig = {
13 | type: FieldType.slider,
14 | name: 'Slider',
15 | group: 'input',
16 | icon: ,
17 | dataType: 'number',
18 | defaultValue: 0,
19 | component: Component,
20 | settings: Settings,
21 | validation: (config: Record) => {
22 | const validation: any[][] = [['number']];
23 |
24 | if (typeof config.min === 'number')
25 | validation.push([
26 | 'min',
27 | config.min,
28 | 'Please use the slider to set the value',
29 | ]);
30 |
31 | if (typeof config.max === 'number')
32 | validation.push([
33 | 'max',
34 | config.max,
35 | 'Please use the slider to set the value',
36 | ]);
37 |
38 | return validation;
39 | },
40 | };
41 | export default SliderConfig;
42 |
--------------------------------------------------------------------------------
/src/Fields/index.ts:
--------------------------------------------------------------------------------
1 | import _find from 'lodash-es/find';
2 | import _get from 'lodash-es/get';
3 | import { IFieldConfig } from '../types';
4 |
5 | import ShortText from './ShortText';
6 | import Paragraph from './Paragraph';
7 | import Date from './Date';
8 | import DateTime from './DateTime';
9 | import Checkbox from './Checkbox';
10 | import Radio from './Radio';
11 | import SingleSelect from './SingleSelect';
12 | import MultiSelect from './MultiSelect';
13 | import Slider from './Slider';
14 | import List from './List';
15 | import Color from './Color';
16 | import Score from './Score';
17 | import Hidden from './Hidden';
18 |
19 | import ContentHeader from './ContentHeader';
20 | import ContentSubHeader from './ContentSubHeader';
21 | import ContentParagraph from './ContentParagraph';
22 | import ContentImage from './ContentImage';
23 |
24 | export const FieldConfigs = [
25 | ShortText,
26 | Paragraph,
27 | Date,
28 | DateTime,
29 | Checkbox,
30 | Radio,
31 | SingleSelect,
32 | MultiSelect,
33 | Slider,
34 | List,
35 | Color,
36 | Score,
37 | Hidden,
38 |
39 | ContentHeader,
40 | ContentSubHeader,
41 | ContentParagraph,
42 | ContentImage,
43 | ];
44 |
45 | /** Returns specific property of field config */
46 | export const getFieldProp = (prop: keyof IFieldConfig, fieldType: string) => {
47 | const field = _find(FieldConfigs, { type: fieldType });
48 | return _get(field, prop);
49 | };
50 |
--------------------------------------------------------------------------------
/src/Form.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useForm, UseFormProps, FieldValues } from 'react-hook-form';
3 | import _isEmpty from 'lodash-es/isEmpty';
4 |
5 | import useFormSettings from './useFormSettings';
6 | import FormFields from './FormFields';
7 | import AutoSave from './AutoSave';
8 | import SubmitButton, { ISubmitButtonProps } from './SubmitButton';
9 | import SubmitError, { ISubmitErrorProps } from './SubmitError';
10 |
11 | import { Fields, CustomComponents } from './types';
12 |
13 | export interface IFormProps {
14 | fields: Fields;
15 | values?: FieldValues;
16 | onSubmit: (
17 | values: FieldValues,
18 | event?: React.BaseSyntheticEvent