Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds "Form.setValues", "createField.getInitialValue", "createField.serialize" #312

Merged
merged 12 commits into from
Oct 26, 2018
Merged
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ SUMMARY.md
# Configurations
.github
.babelrc
babel.config.js
.eslintrc*
.circleci
book.json
Expand All @@ -18,6 +19,7 @@ webpack*
jest*

# Misc
__*
.storybook
favicon.*
logo.*
Expand Down
26 changes: 11 additions & 15 deletions examples/fields/BirthDate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ class BirthDate extends React.Component {
const { fieldProps, fieldState, id, label } = this.props
const { valid, invalid, errors, required } = fieldState

console.log({ errors })

const inputClassNames = [
'form-control',
valid && 'is-valid',
Expand Down Expand Up @@ -58,7 +56,8 @@ class BirthDate extends React.Component {
/>
</div>

{errors &&
{invalid &&
errors &&
errors.map((error, index) => (
<div key={index} className="invalid-feedback d-block">
{error}
Expand All @@ -73,20 +72,17 @@ export default createField({
allowMultiple: true,
valuePropName: 'date',
shouldValidateOnMount: ({ fieldRecord: { date } }) => {
return date.day || date.month || date.year
return date.year || date.month || date.day
},
mapPropsToField: ({ valuePropName, props, fieldRecord }) => {
const date = props.date || ''
const splitDate = date.split('-')
const day = splitDate[0] || ''
const month = splitDate[1] || ''
const year = splitDate[2] || ''
const initialValue = { day, month, year }

getInitialValue(value) {
const parsedDate = value.split('-')
return {
...fieldRecord,
[valuePropName]: initialValue,
initialValue,
year: parsedDate[0] || '',
month: parsedDate[1] || '',
day: parsedDate[2] || '',
}
},
serialize(date) {
return [date.year, date.month, date.day].join('-')
},
})(BirthDate)
12 changes: 10 additions & 2 deletions examples/full/RegistrationForm/RegistrationForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ export default class RegistrationForm extends React.Component {
<React.Fragment>
<h1>Registration form</h1>

<Form action={this.registerUser}>
<Form ref={(form) => (this.form = form)} action={this.registerUser}>
<Field.Group name="primaryInfo">
<Input name="userEmail" type="email" label="E-mail" required />
</Field.Group>

<BirthDate name="birthDate" label="Birth date" required />
<BirthDate
name="birthDate"
label="Birth date"
date="1980-12-10"
required
/>

<Input
name="userPassword"
Expand Down Expand Up @@ -53,6 +58,9 @@ export default class RegistrationForm extends React.Component {
</Field.Group>

<Button>Register</Button>
<button type="reset" onClick={() => this.form.clear()}>
Clear
</button>
</Form>
</React.Fragment>
)
Expand Down
37 changes: 34 additions & 3 deletions src/components/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,36 @@ export default class Form extends React.Component {
)
}

/**
* Accepts the given fields patch and updates the fields
* with that patch. Performs validation for the fields
* present in the patch.
* @param {Object<fieldPath: nextValue>} fieldsPatch
*/
setValues = (fieldsPatch) => {
const { fields } = this.state
const transformers = deriveDeepWith(
(_, nextValue, fieldProps) =>
recordUtils.setValue(fieldProps.getInitialValue(nextValue)),
fieldsPatch,
fields,
)
const nextFields = R.evolve(transformers, fields)

this.setState({ fields: nextFields }, () => {
this.validate((fieldProps) => {
return R.pathSatisfies(
R.complement(R.isNil),
fieldProps.fieldPath,
fieldsPatch,
)
})
})
}

/**
* Updates the fields with the given field record and returns
* the updated state of the fields.
* the updated fields.
* @param {Object} fieldProps
* @returns {Promise}
*/
Expand Down Expand Up @@ -412,6 +439,8 @@ export default class Form extends React.Component {
/**
* Performs the validation of each field in parallel, awaiting for all the pending
* validations to be completed.
* When an optional predicate function is supplied, validates only the fields that
* match the given predicate.
*/
validate = async (predicate = R.T) => {
const { fields } = this.state
Expand Down Expand Up @@ -449,14 +478,16 @@ export default class Form extends React.Component {
}

/**
* Clears the fields matching the given predicate.
* Clears the fields.
* If an optional predicate function is provided, clears
* only the fields that match the given predicate.
*/
clear = (predicate = Boolean) => {
const nextFields = R.compose(
fieldUtils.stitchFields,
R.map(recordUtils.reset(R.always(''))),
R.filter(predicate),
fieldUtils.flattenedFields,
fieldUtils.flattenFields,
)(this.state.fields)

this.setState({ fields: nextFields }, () => {
Expand Down
33 changes: 25 additions & 8 deletions src/components/createField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ const defaultOptions = {
enforceProps() {
return {}
},
serialize(value) {
return value
},
getInitialValue(nextValue) {
return nextValue
},
}

/**
Expand Down Expand Up @@ -124,17 +130,24 @@ export default function connectField(options) {
name: prunedProps.name,
type: prunedProps.type,
valuePropName,
[valuePropName]: registeredValue,
initialValue,
controlled: prunedProps.hasOwnProperty('value'), // TODO Checkboxes are always uncontrolled
[valuePropName]: hocOptions.getInitialValue(registeredValue),
/**
* Store the pristine initial value to assign it
* on reseting the field. "getInitialValue" will be
* invoked with the prisine initialValue during reset
* inside "recordUtils.reset()".
*/
initialValue: initialValue || registeredValue,
controlled: prunedProps.hasOwnProperty(
'value',
) /** @todo Checkboxes are always uncontrolled */,
required: prunedProps.required,
reactiveProps,

//
// TODO
// Debounce an isolate validateField method to handle formless fields
//
/**
* @todo
* Debounce an isolate validateField method to handle formless fields.
*
* When the validate method is debounced on the form level, different
* calls to it from different fields are going to overlap and conflict
* with each other. Wrapping the validate method for each field means
Expand All @@ -149,11 +162,15 @@ export default function connectField(options) {
onFocus: prunedProps.onFocus,
onChange: prunedProps.onChange,
onBlur: prunedProps.onBlur,

/* Internal methods */
getInitialValue: hocOptions.getInitialValue,
serialize: hocOptions.serialize,
}

/* (Optional) Alter the field record using HOC options */
const mappedFieldProps = hocOptions.mapPropsToField({
fieldRecord: initialFieldProps, // TODO Align naming
fieldRecord: initialFieldProps /** @todo Adopt "fieldState" namespace */,
props: prunedProps,
valuePropName,
context,
Expand Down
13 changes: 12 additions & 1 deletion src/utils/fieldUtils/serializeFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import * as R from 'ramda'
import * as recordUtils from '../recordUtils'
import flattenFields from './flattenFields'

/**
* @todo
* Re-write this using functions chain, instead of a single ugly function.
*/
const shouldSerializeField = (fieldProps) => {
if (!fieldProps.fieldPath) {
return
Expand Down Expand Up @@ -33,7 +37,14 @@ const shouldSerializeField = (fieldProps) => {
*/
const serializeFields = R.compose(
R.reduce((acc, fieldProps) => {
return R.assocPath(fieldProps.fieldPath, recordUtils.getValue(fieldProps), acc)
return R.assocPath(
fieldProps.fieldPath,
R.compose(
fieldProps.serialize,
recordUtils.getValue,
)(fieldProps),
acc,
)
}, {}),
R.filter(shouldSerializeField),
flattenFields,
Expand Down
4 changes: 3 additions & 1 deletion src/utils/recordUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export const createField = (initialState) => {
onFocus: null,
onChange: null,
onBlur: null,
getInitialValue: R.always,
serialize: R.always,

...initialState,
}
Expand Down Expand Up @@ -187,7 +189,7 @@ export const resetValidationState = R.mergeDeepLeft({
export const reset = R.curry((nextValueGetter, fieldProps) => {
return R.compose(
// Beware that this will set value to "undefined" when no "initialValue" is found
setValue(nextValueGetter(fieldProps)),
setValue(fieldProps.getInitialValue(nextValueGetter(fieldProps))),
setErrors(null),
resetValidationState,
resetValidityState,
Expand Down