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.

../../../_images/architecture_field.jpg

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 component

  • component - React component for specific input / value element (e.g. DateEdit or DateFormatter)

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 to componentType

    • is defined in fieldFactoryMapping.js

    • to define which UI component should be used

  • dataType to componentConfig

    • 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 and single-select-box dataTypes use select component config but multi-select-box maps to multi-select component and single-select-box maps to single-select component.

  • Same component but different component configs:

    counter and long dataTypes map to integer component but counter uses number component config and long uses integer 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 methods createXXXXXXXField (e.g. createMultipleChoiceField)

  • Combine all form field definitions pseudoFields.map(f => f.formField) and add them the the regular form definition (e.g. use replaceBoxChildren)

  • Combine all entity paths to a single object with getMergedEntityPaths(pseudoFields) (here saved in variable mergedEntityPaths)

  • (Update case only) if you use rest.fetchEntity in combination with form.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 paths entity.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 returns entity and pseudoFields. 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