This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.
Form Field¶
The form field architecture used in the admin package contains a lot of different wrappers and maps. The section should give a rough overview about the architecture.
Glossary¶
dataType
- Tocco form field dataType (e.g.'counter'
)componentType
- Client component type that maps to a component (e.g.'select'
)fieldMappingType
- Defines different dataType to componenType mapping per each fieldMappingType ('readOnly'
|'editable'
|'list'
|'search'
)componentConfig
- Config that gets passed to the componentcomponent
- React component for specific input / value element (e.g.DateEdit
orDateFormatter
)
Packages¶
tocco-ui
contains all form field UI components
redux-form independent
dataType unaware only knows componentType
app-extensions.field
contains a mapping per form type (readOnly, editable, list, search)
mapping maps dataType to componentType (e.g. ‘counter’ maps to ‘integer’ for ‘editable’ fieldMappingType)
app-extensions.formField
adds label, error messages and all styling on top of app-extensions.field for having a complete form field
app-extensions.form
creates redux-form.Field from app-extensions.formField
Mappings¶
There are two different mappings dependent on dataType:
dataType
tocomponentType
is defined in
fieldFactoryMapping.js
to define which UI component should be used
dataType
tocomponentConfig
is defined in
(formatted|editable)ComponentConfigs/index.js
to be able to provide a specific config for the UI component
Therefore it could be that the same component is used with different component configs or vice verca that the same component config is used for different components.
Examples:
Same component config but different components:
multi-select-box
andsingle-select-box
dataTypes useselect
component config butmulti-select-box
maps tomulti-select
component andsingle-select-box
maps tosingle-select
component.Same component but different component configs:
counter
andlong
dataTypes map tointeger
component butcounter
usesnumber
component config andlong
usesinteger
component config.
Event handling¶
redux-form passes event handlers to the field component (ReduxFormFieldAdapter
).
The event handlers are passed down to EditorProvider
which attaches it to the component container div
.
In order to overwrite the default behaviour the specific event has to be attached to the DOM in the edit component.
By calling event.stopPropagation()
the event bubbling can be stopped and the default event handler will not get called.
Example:
const suffixer = val => `${val}-suffix`
const MySuffixEdit = ({value, onChange, events}) => {
const handleBlur = event => {
if (typeof events?.onBlur === 'function') {
events.onBlur(suffixer(event.target.value)) // do not save value as is, always apply suffix
}
event.stopPropagation()
}
return (
<input type="text" value={value} onChange={onChange} onBlur={handleBlur} />
)
}
onBlur event handler¶
The default onBlur
handler by redux-forms sets the text value of the event target as the new value when blurring the input field.
This causes problems as soon there are some non-text value components. Therefore the onBlur
handler is overwritten in the EditorProvider
for all form field to always use the current field value.
Pseudo form fields¶
Sometimes it is necessary to have a fake form or additional fields in a form which does not really exist in the entity model.
Such fields are called pseudo fields as the backend does not know them. A part from regular data types such as text
or string
you can add single and multi selection fields with the data type choice
. The form
package provides helper methods to create fields
with createXXXXXXXField
(e.g. createMultipleChoiceField
see (See JSDoc for all helper methods) and to create answer options for choice fields with createChoiceOption
.
Such fields are normally synchronous validated. For the choice
data type is a special syncTypeValidator
implemented.
The asynchronous validation removes these pseudo fields before sending the request to the backend.
Each pseudo field object (returned by createXXXXXXXField
) have two parts formField
the form field definition and entityPath
which should be added to the entity path of the regular entity.
The following steps are required to use pseudo fields with the form builder:
Create a list of pseudo fields (here named
pseudoFields
) with the helper methodscreateXXXXXXXField
(e.g.createMultipleChoiceField
)Combine all form field definitions
pseudoFields.map(f => f.formField)
and add them the the regular form definition (e.g. usereplaceBoxChildren
)Combine all entity paths to a single object with
getMergedEntityPaths(pseudoFields)
(here saved in variablemergedEntityPaths
)(Update case only) if you use
rest.fetchEntity
in combination withform.getUsedPaths
you must exclude the pseudo fields (e.g.yield call(form.getUsedPaths, fieldDefinitions, field => field.pseudoField !== true)
) else the backend throws an error due to unkown paths(Update case only) after fetching the entity (saved in variable
entity
) combine them with the pseudo field entity pathsentity.paths = {...entity.paths, ...mergedEntityPaths}
(Create case only) the entity paths must be transformed to form values as no entity paths are loaded. This can be done with:
const defaultValues = yield call(form.getDefaultValues, fieldDefinitions)
const entityValues = yield call(api.getFlattenEntity, {model: 'MODEL', paths: {...mergedEntityPaths}})
const formValues = yield call(form.entityToFormValues, {...entityValues, ...defaultValues}, fieldDefinitions)
Now you can initialize redux with
yield put(formActions.initialize(REDUX_FORM_NAME, formValues))
(Note: if you call the redux initialize event before adding the pseudo fields the initial values are not set correctly)If the form is submitted you can split the form values in to the regular entity and pseudoFields with
splitFlattenEntity
which returnsentity
andpseudoFields
. For example:
const flatEntity = yield call(form.formValuesToFlattenEntity, formValues, fieldDefinitions)
const splittedFlatEntity = form.splitFlattenEntity(flatEntity, fieldDefinitions)
const regularEntity = yield call(api.toEntity, splittedFlatEntity.entity)
// do something with splittedFlatEntity.pseudoFields