Skip to content

Commit

Permalink
feat: add support for required fields and validation
Browse files Browse the repository at this point in the history
Prevents importing without a file, causing a runtime-error
and forever loading.
  • Loading branch information
Birkbjo committed Feb 1, 2019
1 parent 2f2775d commit 1976778
Show file tree
Hide file tree
Showing 18 changed files with 305 additions and 56 deletions.
7 changes: 5 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2019-02-01T15:53:30.758Z\n"
"PO-Revision-Date: 2019-02-01T15:53:30.758Z\n"
"POT-Creation-Date: 2019-02-01T16:01:43.052Z\n"
"PO-Revision-Date: 2019-02-01T16:01:43.052Z\n"

msgid "user is not logged in"
msgstr ""
Expand All @@ -32,6 +32,9 @@ msgstr ""
msgid "Choose a file to upload"
msgstr ""

msgid "Required"
msgstr ""

msgid "more options"
msgstr ""

Expand Down
40 changes: 26 additions & 14 deletions src/components/Form/File/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SvgIcon } from 'material-ui'
import { FormControl, FormLabel } from '../material-ui'
import s from './styles.css'
import i18n from '@dhis2/d2-i18n'
import { isValueNil } from 'helpers'

function FileUploadIcon() {
return (
Expand All @@ -21,25 +22,36 @@ export default class FileField extends React.Component {
onChange = () => this.props.onChange(this.props.name, this.fileRef.files[0])

render() {
const { selected } = this.props
let label = this.props.label
const { selected, required, formMeta } = this.props
let label =
this.props.label || label || i18n.t('Choose a file to upload')
let helpText = this.props.helpText
if (selected) {
label = selected.name
}
if (required) {
label += ' *'
if (formMeta.submitted && isValueNil(selected)) {
helpText = i18n.t('Required')
}
}

return (
<FormControl className={s.formControl} onClick={this.onClick}>
<input
type="file"
onChange={this.onChange}
ref={c => (this.fileRef = c)}
className={s.hiddenFileInput}
/>
<FileUploadIcon className={s.button} />
<FormLabel className={s.formLabel}>
{label || i18n.t('Choose a file to upload')}
</FormLabel>
</FormControl>
<React.Fragment>
<FormControl className={s.formControl} onClick={this.onClick}>
<input
type="file"
onChange={this.onChange}
ref={c => (this.fileRef = c)}
className={s.hiddenFileInput}
/>
<FileUploadIcon className={s.button} />
<FormLabel className={s.formLabel}>
{label || i18n.t('Choose a file to upload')}
</FormLabel>
</FormControl>
{helpText && <p className={s.helpText}>{helpText}</p>}
</React.Fragment>
)
}
}
11 changes: 11 additions & 0 deletions src/components/Form/File/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@ form .formLabel {
.hiddenFileInput {
display: none;
}

form .helpText {
height: 12px;
padding-left: 14px;
font-size: 12px;

line-height: 12px;

cursor: help;
color: #d32f2f;
}
6 changes: 3 additions & 3 deletions src/components/Form/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ export * from './constants'
export class Form extends React.Component {
fields() {
const { fields, fieldValues } = this.props
const { _context: context } = fieldValues
const { _context: context, _meta: formMeta } = fieldValues

return fields.map(field => {
if (field.context !== CTX_DEFAULT && field.context !== context) {
return null
}

const { type, name, label, className } = field
const props = { name, label, className }
const { type, name, label, className, required } = field
const props = { name, label, className, required, formMeta }

if (type === TYPE_RADIO) {
props['values'] = fieldValues[name]['values']
Expand Down
68 changes: 53 additions & 15 deletions src/components/FormBase/index.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,89 @@
import React from 'react'
import { getFieldState, getFieldValue } from 'helpers'
import {
getFieldState,
getFieldValue,
hasRequiredFieldsWithoutValue,
} from 'helpers'
import { Form, Loading, Error } from 'components'

import s from './styles.css'

export class FormBase extends React.Component {
onChange = (name, value) =>
onChange = (name, value) => {
const valid = this.props.validateOnChange
? this.validate()
: this.state._meta.valid
this.setState(
{
...getFieldState(name, value, this.fields, this.state),
...this.setMetaState({ valid }),
},
() => {
this.onFormUpdate && this.onFormUpdate(name, value)
}
)
}

changeContext = _context => this.setState({ _context })

getFormState() {
const values = {}
this.fields.map(f => f.name).forEach(name => {
if (name) {
values[name] = getFieldValue(this.state[name])
}
})
this.fields
.map(f => f.name)
.forEach(name => {
if (name) {
values[name] = getFieldValue(this.state[name])
}
})
return values
}

setProcessing = () => this.setState({ processing: true })
clearProcessing = () => this.setState({ processing: false })
setMetaState(metaState, cb = undefined) {
return this.setState(
state => ({
_meta: {
...state._meta,
...metaState,
},
}),
cb
)
}

setProcessing = () => this.setMetaState({ processing: true })
clearProcessing = () => this.setMetaState({ processing: false })

onClearError = () => this.setMetaState({ error: false })

onClearError = () => this.setState({ error: null })
assertOnError = evt => {
try {
const { message: error } = JSON.parse(evt.target.response)
this.setState({ error, processing: false })
this.setMetaState({ error, processing: false })
} catch (err) {}
}

validate = () => {
// Simple default validation for now:
// just check if there exists required fields without a value
return !hasRequiredFieldsWithoutValue(this.fields, this.state)
}
onBeforeSubmit = () => {
this.setMetaState({ submitted: true })
const valid = this.validate()
return valid && this.onSubmit()
}

render() {
if (this.state.error) {
if (this.state._meta.error) {
return (
<Error message={this.state.error} onClear={this.onClearError} />
<Error
message={this.state._meta.error}
onClear={this.onClearError}
/>
)
}

if (this.state.processing) {
if (this.state._meta.processing) {
return <Loading />
}

Expand All @@ -63,7 +101,7 @@ export class FormBase extends React.Component {
onChange={this.onChange}
changeContext={this.changeContext}
submitLabel={this.submitLabel}
onSubmit={this.onSubmit}
onSubmit={this.onBeforeSubmit}
/>
)
}
Expand Down
138 changes: 137 additions & 1 deletion src/helpers/__tests__/helpers.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { today } from '../date'
import { getField } from '../form'
import {
getField,
getRequiredFields,
hasRequiredFieldsWithoutValue,
} from '../form'
import { isValueNil } from '../values'

describe('Date', () => {
it('today() is not null', () => {
Expand All @@ -24,4 +29,135 @@ describe('Form', () => {
const f = getField(selected, fields)
expect(f.name === selected).toBe(true)
})

describe('getRequiredFields', () => {
it('should return a list with only required fields', () => {
const fields = [
{
name: 'test',
required: true,
},
{
name: 'test',
required: false,
},
{
name: 'test',
},
]
const f = getRequiredFields(fields)
f.forEach(f => expect(f).toHaveProperty('required', true))
})
it('should return empty list if required is not specified', () => {
const fields = [
{
name: 'test',
},
{
name: 'test',
},
{
name: 'test',
},
]
const f = getRequiredFields(fields)
f.forEach(f => expect(f).toHaveProperty('required', true))
})
})

describe('hasRequiredFieldsWithoutValue', () => {
it('should return false when fields have values', () => {
const fields = [
{ name: 'Birk', required: true },
{ name: 'Kjetil' },
]
const fieldValues = {
Birk: {
selected: 'Cool',
},
Kjetil: {
selected: 'Traitor',
},
}

expect(hasRequiredFieldsWithoutValue(fields, fieldValues)).toBe(
false
)
})
it('should return true when fields are required and have no value', () => {
const fields = [
{ name: 'Birk', required: true },
{ name: 'Kjetil', required: false },
]
const fieldValues = {
Birk: {
selected: null,
},
Kjetil: {
selected: 'Traitor',
},
}
expect(hasRequiredFieldsWithoutValue(fields, fieldValues)).toBe(
true
)
})
it('should return false when fields do not have required flag', () => {
const fields = [{ name: 'Ameen' }, { name: 'Stian' }]
const fieldValues = {
Ameen: {
selected: null,
},
Stian: {
selected: 'Flink',
},
}
expect(hasRequiredFieldsWithoutValue(fields, fieldValues)).toBe(
false
)
})

it('should return true when fields are required and value is empty string', () => {
const fields = [
{ name: 'Ameen' },
{ name: 'Stian', required: true },
]
const fieldValues = {
Ameen: {
selected: null,
},
Stian: {
selected: '',
},
}
expect(hasRequiredFieldsWithoutValue(fields, fieldValues)).toBe(
true
)
})
})
})

describe('Values', () => {
describe('isValueNil', () => {
it('should return true when value is null', () => {
expect(isValueNil(null)).toBe(true)
})

it('should return true when value is undefined', () => {
expect(isValueNil(undefined)).toBe(true)
})

it('should return true when value is empty string', () => {
expect(isValueNil('')).toBe(true)
})

it('should return false when value is false', () => {
expect(isValueNil(false)).toBe(false)
})

it('should return false when value is a value', () => {
expect(isValueNil('test')).toBe(false)
expect(isValueNil(5)).toBe(false)
expect(isValueNil({ test: 'test' })).toBe(false)
})
})
})
5 changes: 3 additions & 2 deletions src/helpers/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import {
} from 'components/Form'
import { TYPE_RADIO, TYPE_SCHEMAS } from 'components/Form'

function getField(name, label, type, context = CTX_DEFAULT) {
function getField(name, label, type, context = CTX_DEFAULT, required = false) {
return {
context,
type,
name,
label,
required,
}
}

Expand Down Expand Up @@ -139,7 +140,7 @@ const fields = {
),
startDate: getField('startDate', i18n.t('Start date'), TYPE_DATE),
strategy: getField('strategy', i18n.t('Strategy'), TYPE_RADIO),
upload: getField('upload', null, TYPE_FILE),
upload: getField('upload', null, TYPE_FILE, undefined, true),
}

export function getFormField(name, options = {}) {
Expand Down
Loading

0 comments on commit 1976778

Please sign in to comment.