@scenid/react-formulator 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.eslintignore +21 -0
  2. package/.eslintrc +70 -0
  3. package/.firebase/hosting.c3Rvcnlib29rLXN0YXRpYw.cache +38 -0
  4. package/.firebaserc +5 -0
  5. package/.storybook/main.js +12 -0
  6. package/.storybook/preview.js +9 -0
  7. package/README.md +29 -0
  8. package/dist/index.cjs.css +96 -0
  9. package/dist/index.cjs.js +25 -0
  10. package/dist/index.esm.css +96 -0
  11. package/dist/index.esm.js +25 -0
  12. package/firebase.json +17 -0
  13. package/package.json +74 -0
  14. package/rollup.config.js +35 -0
  15. package/src/Editable/FormBoolean.jsx +44 -0
  16. package/src/Editable/FormField.jsx +223 -0
  17. package/src/Editable/FormNumber.jsx +36 -0
  18. package/src/Editable/FormRepeater.jsx +190 -0
  19. package/src/Editable/FormSelect.jsx +49 -0
  20. package/src/Editable/FormTextfield.jsx +13 -0
  21. package/src/FormGroupHeader.jsx +85 -0
  22. package/src/FormHelpers.js +191 -0
  23. package/src/FormSectionBlock.jsx +70 -0
  24. package/src/FormSectionCard.jsx +62 -0
  25. package/src/FormulatorForm.jsx +537 -0
  26. package/src/FormulatorFormSection.jsx +494 -0
  27. package/src/HiddenData.jsx +24 -0
  28. package/src/ReadOnly/FormBoolean.jsx +36 -0
  29. package/src/ReadOnly/FormField.jsx +126 -0
  30. package/src/ReadOnly/FormMarkdown.jsx +20 -0
  31. package/src/ReadOnly/FormNumber.jsx +17 -0
  32. package/src/ReadOnly/FormReadOnlyText.jsx +19 -0
  33. package/src/ReadOnly/FormRepeater.jsx +36 -0
  34. package/src/ReadOnly/FormSelect.jsx +18 -0
  35. package/src/helpers.js +13 -0
  36. package/src/index.js +3 -0
  37. package/stories/Forms.stories.jsx +126 -0
  38. package/stories/Introduction.stories.mdx +206 -0
  39. package/stories/StoryBase.jsx +35 -0
  40. package/stories/assets/code-brackets.svg +1 -0
  41. package/stories/assets/colors.svg +1 -0
  42. package/stories/assets/comments.svg +1 -0
  43. package/stories/assets/direction.svg +1 -0
  44. package/stories/assets/flow.svg +1 -0
  45. package/stories/assets/plugin.svg +1 -0
  46. package/stories/assets/repo.svg +1 -0
  47. package/stories/assets/stackalt.svg +1 -0
  48. package/stories/forms/login.render.schema.json +23 -0
  49. package/stories/forms/login.validation.schema.json +29 -0
  50. package/stories/forms/markdown.render.schema.json +30 -0
  51. package/stories/forms/markdown.validation.schema.json +18 -0
  52. package/stories/forms/register.render.schema.json +32 -0
  53. package/stories/forms/register.validation.schema.json +34 -0
  54. package/stories/forms/types.schemas.js +171 -0
@@ -0,0 +1,537 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import EventEmitter from 'eventemitter3'
5
+ import { validate as validator } from '@scenid/formulator'
6
+ import isEqual from 'deep-equal'
7
+
8
+ import { render as mm } from 'micromustache'
9
+
10
+ import FormulatorFormSection from './FormulatorFormSection'
11
+ import HiddenData from './HiddenData'
12
+
13
+ import 'highlight.js/styles/shades-of-purple.css'
14
+
15
+ const asyncValidatorEnvelope = async (path, func) => {
16
+ const r = await func()
17
+
18
+ if (r !== true) {
19
+ return ({
20
+ path,
21
+ validator: 'Async',
22
+ message: r
23
+ })
24
+ }
25
+
26
+ return true
27
+ }
28
+
29
+ const validate = async (ee, schema, data, oldAsyncErrors, fieldStates, asyncValidators) => {
30
+ const { errors, results, ...rest } = validator(schema, data)
31
+
32
+ let asyncResults = []
33
+ let asyncErrors = oldAsyncErrors
34
+
35
+ if (asyncValidators) {
36
+ const activeValidators = (
37
+ Object
38
+ .entries(asyncValidators)
39
+ .map(e => ({ path: e[0], func: e[1] }))
40
+ .filter(({ path }) => results[path].sanitizedData !== undefined)
41
+ )
42
+
43
+ if (activeValidators.length > 0) {
44
+ const newFieldStates = JSON.parse(JSON.stringify(fieldStates))
45
+ activeValidators.forEach(({ path }) => { newFieldStates[path].validating = true })
46
+ ee.emit('fieldStates', newFieldStates)
47
+
48
+ asyncResults = await Promise.all(
49
+ activeValidators
50
+ .map(({ path, func }) => {
51
+ if (data[path]) {
52
+ return asyncValidatorEnvelope(
53
+ path,
54
+ () => func(path, data[path])
55
+ )
56
+ }
57
+ return Promise.resolve(true)
58
+ })
59
+ )
60
+
61
+ asyncErrors = asyncResults.filter(m => m !== true)
62
+ }
63
+ }
64
+
65
+ const mergedErrors = [...errors, ...asyncErrors]
66
+
67
+ const mergedResults = { ...results }
68
+ asyncErrors.forEach(e => {
69
+ const fr = mergedResults[e.path]
70
+ fr.errors = [...fr.errors, e]
71
+ fr.hasErrors = true
72
+ fr.sanitizedData = undefined
73
+ })
74
+
75
+ ee.emit(
76
+ 'validation',
77
+ {
78
+ ...rest,
79
+ hasErrors: Object.keys(mergedErrors).length > 0,
80
+ errors: mergedErrors,
81
+ asyncErrors,
82
+ results: mergedResults,
83
+ fieldStates
84
+ }
85
+ )
86
+ }
87
+
88
+ const validateSync = (schema, data) => validator(schema, data)
89
+
90
+ const getInitialData = (schema, iniData, transientData, iniFieldStates) => {
91
+ const { hasErrors, errors, results, data, sanitizedData } = validateSync(schema, { ...iniData, ...transientData })
92
+
93
+ const fieldStates = iniFieldStates || (
94
+ Object
95
+ .keys(results)
96
+ .map(path => ({
97
+ [path]: {
98
+ validating: false,
99
+ active: false,
100
+ touched: false,
101
+ dirty: false,
102
+ inactive: true,
103
+ pristine: true,
104
+ clean: true
105
+ }
106
+ }))
107
+ .reduce((l, r) => ({ ...l, ...r }), {})
108
+ )
109
+
110
+ const finalData = (
111
+ Object
112
+ .entries(data)
113
+ .map(e => {
114
+ let value = e[1]
115
+ if (value === undefined && sanitizedData[e[0]] !== undefined) value = sanitizedData[e[0]]
116
+ return { [e[0]]: value }
117
+ })
118
+ .reduce((l, r) => ({ ...l, ...r }), {})
119
+ )
120
+
121
+ return ({
122
+ preSubmitValidation: false,
123
+ hasErrors,
124
+ isDirty: Object.values(fieldStates).reduce((l, r) => l || r.dirty, false),
125
+ errors,
126
+ results,
127
+ data: finalData,
128
+ asyncErrors: [],
129
+ fieldStates
130
+ })
131
+ }
132
+
133
+ class FormulatorForm extends React.Component {
134
+ constructor(props) {
135
+ super(props)
136
+
137
+ const { schema, data, transientData, fieldStates, onStateChange } = props
138
+
139
+ this.state = getInitialData(schema, data, transientData, fieldStates)
140
+
141
+ this.handleFieldChange = this.handleFieldChange.bind(this)
142
+ this.handleFocus = this.handleFocus.bind(this)
143
+ this.handleBlur = this.handleBlur.bind(this)
144
+
145
+ this.ee = new EventEmitter()
146
+
147
+ this.ee.addListener('validation', this.handleValidation.bind(this))
148
+ if (onStateChange) this.ee.addListener('validation', onStateChange)
149
+
150
+ this.ee.addListener('fieldStates', this.handleFieldStates.bind(this))
151
+ }
152
+
153
+ componentDidMount() {
154
+ this.reportStatus()
155
+ }
156
+
157
+ componentDidUpdate(prevProps) {
158
+ const { data, transientData } = this.props
159
+
160
+ if (!isEqual(prevProps.data, data)) {
161
+ this.reinitialize()
162
+ }
163
+
164
+ if (transientData && !isEqual(transientData, prevProps.transientData)) {
165
+ this.forceFieldChanges(
166
+ Object
167
+ .entries(transientData)
168
+ .filter(e => e[1] !== prevProps.data[e[0]])
169
+ .map(e => ({ name: e[0], value: e[1] }))
170
+ )
171
+ }
172
+ }
173
+
174
+ componentWillUnmount() {
175
+ const { hasErrors, isDirty, data, fieldStates } = this.state
176
+ const { onUnmount } = this.props
177
+
178
+ if (onUnmount) onUnmount({ hasErrors, isDirty, data, fieldStates })
179
+ this.ee.removeAllListeners()
180
+ }
181
+
182
+ reinitialize() {
183
+ const { schema, data, transientData } = this.props
184
+
185
+ this.setState(getInitialData(schema, data, transientData))
186
+ }
187
+
188
+ forceFieldChanges(transientData) {
189
+ const { schema, data: initData, disabled, asyncValidators } = this.props
190
+ const { preSubmitValidation, data: oldData, asyncErrors, fieldStates } = this.state
191
+
192
+ if (preSubmitValidation || disabled) return
193
+
194
+ const newData = { ...oldData }
195
+ let newAsyncErrors = []
196
+ const newFieldStates = { ...fieldStates }
197
+
198
+ transientData.forEach(({ name, value }) => {
199
+ let finalValue = value
200
+ if (schema.properties[name].type === 'boolean') finalValue = oldData[name] !== true
201
+
202
+ const isDirty = initData[name] !== value
203
+
204
+ newAsyncErrors = asyncErrors.filter(({ path }) => path !== name).concat(newAsyncErrors)
205
+
206
+ newFieldStates[name] = {
207
+ ...fieldStates[name],
208
+ dirty: isDirty,
209
+ clean: !isDirty
210
+ }
211
+
212
+ newData[name] = finalValue
213
+ })
214
+
215
+ validate(
216
+ this.ee,
217
+ schema,
218
+ newData,
219
+ newAsyncErrors,
220
+ newFieldStates,
221
+ asyncValidators
222
+ )
223
+ }
224
+
225
+ handleFieldChange(e) {
226
+ const { name, value } = e.target
227
+
228
+ const { schema, data: initData, disabled, asyncValidators } = this.props
229
+ const { preSubmitValidation, data: oldData, asyncErrors, fieldStates } = this.state
230
+
231
+ if (preSubmitValidation || disabled) return
232
+
233
+ const isDirty = initData[name] !== value
234
+
235
+ const newAsyncErrors = asyncErrors.filter(({ path }) => path !== name)
236
+
237
+ validate(
238
+ this.ee,
239
+ schema,
240
+ { ...oldData, [name]: value },
241
+ newAsyncErrors,
242
+ {
243
+ ...fieldStates,
244
+ [name]: {
245
+ ...fieldStates[name],
246
+ dirty: isDirty,
247
+ clean: !isDirty
248
+ }
249
+ },
250
+ asyncValidators
251
+ )
252
+ }
253
+
254
+ handleValidation({ hasErrors, fieldStates, errors, ...result }) {
255
+ const { onSubmit, onStatusUpdate } = this.props
256
+ const { preSubmitValidation } = this.state
257
+
258
+ const statusFields = {
259
+ preSubmitValidation: false,
260
+ hasErrors,
261
+ errors,
262
+ isDirty: Object.values(fieldStates).reduce((l, r) => l || r.dirty, false)
263
+ }
264
+
265
+ this.setState(
266
+ { ...statusFields, fieldStates, errors, ...result },
267
+ () => {
268
+ if (onStatusUpdate) onStatusUpdate(statusFields)
269
+
270
+ if (preSubmitValidation && !hasErrors && onSubmit) onSubmit(result.sanitizedData)
271
+ }
272
+ )
273
+ }
274
+
275
+ reportStatus() {
276
+ const { onStatusUpdate } = this.props
277
+ const { hasErrors, fieldStates } = this.state
278
+
279
+ const statusFields = {
280
+ preSubmitValidation: false,
281
+ hasErrors,
282
+ isDirty: Object.values(fieldStates).reduce((l, r) => l || r.dirty, false)
283
+ }
284
+
285
+ if (onStatusUpdate) onStatusUpdate(statusFields)
286
+ }
287
+
288
+ handleFieldStates(fieldStates) {
289
+ this.setState({ fieldStates })
290
+ }
291
+
292
+ handleSubmit() {
293
+ const { schema, asyncValidators, onStatusUpdate } = this.props
294
+ const { errors, hasErrors, data, asyncErrors, fieldStates } = this.state
295
+
296
+ this.setState(
297
+ { preSubmitValidation: true },
298
+ () => {
299
+ const statusFields = {
300
+ preSubmitValidation: true,
301
+ hasErrors,
302
+ errors,
303
+ isDirty: Object.values(fieldStates).reduce((l, r) => l || r.dirty, false)
304
+ }
305
+
306
+ if (onStatusUpdate) onStatusUpdate(statusFields)
307
+
308
+ validate(this.ee, schema, data, asyncErrors, fieldStates, asyncValidators)
309
+ }
310
+ )
311
+ }
312
+
313
+ setFieldState(path, flags) {
314
+ const { fieldStates } = this.state
315
+
316
+ this.setState({
317
+ fieldStates: {
318
+ ...fieldStates,
319
+ [path]: {
320
+ ...fieldStates[path],
321
+ ...flags
322
+ }
323
+ }
324
+ })
325
+ }
326
+
327
+ handleFocus(e) {
328
+ const path = e.target.name
329
+
330
+ this.setFieldState(
331
+ path,
332
+ {
333
+ active: true,
334
+ inactive: false
335
+ }
336
+ )
337
+ }
338
+
339
+ handleBlur(e) {
340
+ const path = e.target.name
341
+
342
+ this.setFieldState(
343
+ path,
344
+ {
345
+ active: false,
346
+ inactive: true,
347
+ touched: true,
348
+ pristine: false
349
+ }
350
+ )
351
+ }
352
+
353
+ render() {
354
+ const {
355
+ id,
356
+ variant,
357
+ inputVariant,
358
+ groupLabelVariant,
359
+ groupDescVariant,
360
+ textAlign,
361
+ readOnly,
362
+ listErrors,
363
+ schema,
364
+ hideSensitiveData,
365
+ renderSchema,
366
+ disabled: propDisabled,
367
+ translations,
368
+ componentMap,
369
+ renderComponentMap,
370
+ errorMessages,
371
+ onReset
372
+ } = this.props
373
+
374
+ const {
375
+ hasErrors,
376
+ errors,
377
+ results,
378
+ data,
379
+ fieldStates,
380
+ preSubmitValidation
381
+ } = this.state
382
+
383
+ const disabled = preSubmitValidation || propDisabled
384
+
385
+ return (
386
+ <form
387
+ id={id}
388
+ className="formulator-form"
389
+ disabled={disabled}
390
+ onSubmit={e => {
391
+ e.preventDefault()
392
+ if (!disabled) this.handleSubmit()
393
+ }}
394
+ onReset={e => {
395
+ e.preventDefault()
396
+ this.reinitialize()
397
+ if (onReset) onReset()
398
+ }}
399
+ >
400
+ {
401
+ renderSchema.title
402
+ && <h5>{renderSchema.title}</h5>
403
+ }
404
+ {
405
+ renderSchema.description
406
+ && <h5>{renderSchema.description}</h5>
407
+ }
408
+ {
409
+ renderSchema.groups
410
+ .map(({ id: sectionId, label, description, fields }) => {
411
+ if (hideSensitiveData && sectionId === 'bankData') {
412
+ return <HiddenData subject="Der Bereich" label={label} />
413
+ }
414
+
415
+ return (
416
+ <FormulatorFormSection
417
+ key={`form-section-${sectionId}`}
418
+ id={id}
419
+ sectionId={sectionId}
420
+ variant={variant}
421
+ inputVariant={inputVariant}
422
+ labelVariant={groupLabelVariant}
423
+ descVariant={groupDescVariant}
424
+ textAlign={textAlign}
425
+ readOnly={readOnly}
426
+ disabled={disabled}
427
+ schema={schema}
428
+ label={label}
429
+ desc={description}
430
+ fields={fields}
431
+ hideSensitiveData={hideSensitiveData}
432
+ results={results}
433
+ data={data}
434
+ fieldStates={fieldStates}
435
+ translations={translations}
436
+ errorMessages={errorMessages}
437
+ componentMap={componentMap}
438
+ renderComponentMap={renderComponentMap}
439
+ onChange={this.handleFieldChange}
440
+ />
441
+ )
442
+ })
443
+ }
444
+ {
445
+ (listErrors && hasErrors)
446
+ && (
447
+ <div className="errors">
448
+ {
449
+ errors.map(({ path, message, options, validator: eVal }) => {
450
+ let finalMessage = message
451
+
452
+ if (options && options.human) finalMessage = options.human
453
+ else if (errorMessages[eVal]) finalMessage = mm(errorMessages[eVal], options)
454
+
455
+ return (
456
+ <div
457
+ key={`form-error-${path}-${validator}`}
458
+ className="error"
459
+ >
460
+ <div className="field">{translations.labels[path] || path}</div>
461
+ <div className="message">
462
+ {finalMessage}
463
+ </div>
464
+ </div>
465
+ )
466
+ })
467
+ }
468
+ </div>
469
+ )
470
+ }
471
+ </form>
472
+ )
473
+ }
474
+ }
475
+
476
+ FormulatorForm.propTypes = {
477
+ id: PropTypes.string.isRequired,
478
+ variant: PropTypes.oneOf(['card', 'block']),
479
+ inputVariant: PropTypes.oneOf(['standard', 'filled', 'outlined']),
480
+ textAlign: PropTypes.oneOf(['left', 'center', 'right']),
481
+ groupLabelVariant: PropTypes.oneOf([
482
+ 'h1',
483
+ 'h2',
484
+ 'h3',
485
+ 'h4',
486
+ 'h5',
487
+ 'h6',
488
+ 'subtitle1',
489
+ 'subtitle2',
490
+ 'body1',
491
+ 'body2',
492
+ 'button',
493
+ 'caption',
494
+ 'overline'
495
+ ]),
496
+ groupDescVariant: PropTypes.oneOf([
497
+ 'h1',
498
+ 'h2',
499
+ 'h3',
500
+ 'h4',
501
+ 'h5',
502
+ 'h6',
503
+ 'subtitle1',
504
+ 'subtitle2',
505
+ 'body1',
506
+ 'body2',
507
+ 'button',
508
+ 'caption',
509
+ 'overline'
510
+ ]),
511
+ disabled: PropTypes.bool,
512
+ readOnly: PropTypes.bool,
513
+ listErrors: PropTypes.bool,
514
+ schema: PropTypes.object.isRequired,
515
+ data: PropTypes.object.isRequired,
516
+ hideSensitiveData: PropTypes.bool,
517
+ transientData: PropTypes.object,
518
+ fieldStates: PropTypes.object,
519
+ renderSchema: PropTypes.object.isRequired,
520
+ translations: PropTypes.object.isRequired,
521
+ asyncValidators: PropTypes.object,
522
+ errorMessages: PropTypes.object,
523
+ componentMap: PropTypes.object,
524
+ renderComponentMap: PropTypes.object,
525
+ onSubmit: PropTypes.func,
526
+ onReset: PropTypes.func,
527
+ onStatusUpdate: PropTypes.func,
528
+ onStateChange: PropTypes.func,
529
+ onUnmount: PropTypes.func
530
+ }
531
+
532
+ FormulatorForm.defaultProps = {
533
+ variant: 'card',
534
+ errorMessages: {}
535
+ }
536
+
537
+ export default FormulatorForm