Core: form.io integration
Open Forms uses form.io under the hood to build and render forms, and then adds its own layers on top of that, such as:
implementing multi-step forms where every step is a form.io definition
evaluating backend logic using data from earlier steps
dynamically adapting form.io definitions as needed
This means that we process the form.io datastructures in the backend, using Python. All
the code for this is organized in the openforms.formio package.
Changed in version 2.1.0: openforms.custom_field_types was refactored into the openforms.formio package,
and all of the separate registries (formatters, normalizers…) were merged into a
single compoment registry.
Form.io configuration
A form.io configuration is an object containing a "components" key mapped to an array of objects
representing the definition of form components for a specific form step.
The following is an example of such a configuration:
{
"display": "form",
"components": [
{
"type": "textfield",
"key": "field_1",
"label": "Field 1"
},
{
"type": "number",
"key": "field_2",
"label": "Field 2"
}
]
}
Whenever a submission is created, submission data will be attached to it. The layout of this submission data will depend on the components configuration. For instance, with the example configuration given above, submission data will look like:
{
"field_1": "some_value",
"field_2": 1
}
Components can be roughly categorised as layout and data components. Layout components don’t have a matching entry in the submission data.
Every component has two required properties:
"key": A unique identifier across the form. The key represents a structured path “into” the submission data. A period (.) represents a level of nesting in this data."type": The corresponding component type.
Note
Submission data should be interpreted along with components configuration, as it is impossible to determine how data needs to be handled without this context. At times, the submission data can also influence the component configuration, e.g. with conditionals expressing when a component is visible or not.
The form.io playground can be used to play with the different components and how the submission data will look like.
Supported features
Formatting values for display
Value formatting is done for displaying form submission data summaries, rendering confirmation PDFs and emails… It is aware if it’s in a HTML context or not. It is heavily used in the renderers.
Whenever a component plugin is registered, the
openforms.formio.registry.BasePlugin.formatter class attribute must be
specified.
- openforms.formio.service.format_value(component: Component, value: Any, *, as_html: bool = False)
Format a submitted value in a way that is most appropriate for the component type.
Normalizing input data
Data for a component can be sourced from external systems that employ different formatting rules compared to what form.io expects. Normalizing this data helps to be able to make proper comparisons at different stages in the submission life-cycle.
You can opt-in to this by configuring openforms.formio.registry.BasePlugin.normalizer.
- openforms.formio.service.normalize_value_for_component(component: Component, value: Any) Any
Given a value (actual or default value) and the component, apply the component- specific normalization.
Dynamically modifying component configuration
Certain component types require on-the-fly configuration rewriting, such as applying global configuration options that may change independently from when the form is actually being designed.
Dynamic rewriting is enabled by implementing
openforms.formio.registry.BasePlugin.mutate_config_dynamically(). It receives the
current openforms.submissions.models.Submission instance and a mapping of all
the variable names and values at the time.
- openforms.formio.service.get_dynamic_configuration(config_wrapper: FormioConfigurationWrapper, submission: Submission, data: FormioData | None = None) FormioConfigurationWrapper
Given a static Formio configuration, apply the hooks to dynamically transform this.
The configuration is modified in the context of the provided
submissionparameter.
For an example of a custom field type, see openforms.formio.components.custom.Date.
Finally, the resulting resolved component definitions are evaluated with the template
engine where variable values are evaluated for compoment labels, descriptions… and
configuration based on the HTTP request is performed (see
openforms.formio.service.rewrite_formio_components_for_request()).
Pre-registration of components
Some component types have specific logic which should be executed after the form is submitted, for example updating an external service using the submission data.
This logic can be implemented in the openforms.formio.registry.BasePlugin.pre_registration_hook().
This method is called for all components during the
openforms.submissions.tasks.on_post_submission_event() proces, between the pre-pregistration
and registration steps, so the submission reference ID is already generated and can be used.
For an example of a custom field type, see openforms.formio.components.custom.CustomerProfile.
The complete registration flow is described in the Core: submission processing.
Reference
Public API - openforms.formio.service
Expose the public openforms.formio Python API.
The service module exposes the functions/utilities that may be used by other Django apps/packages:
Try to keep this module stable and avoid breaking changes - extensions may rely on this!
Keep it small! The actual implementation should be done in specialized subpackages or submodules and only their ‘public’ API should be imported and used.
- exception openforms.formio.service.DuplicateKeyError(key: str, *args)
Error raised for unique component key violations.
- class openforms.formio.service.FormioConfigurationWrapper(configuration: FormioConfiguration, *, validate_unique_keys: bool = False)
Wrap around the Formio configuration dictionary for further processing.
This datastructure caches the internal datastructure to optimize mutations of the formio configuration.
- get_child_component_keys(key: str) set[str]
Get all child component keys for an arbitrary component key.
- Parameters:
key – The component key.
- Returns:
A set of children component keys. Will be an empty set if the component does not contain any children.
- class openforms.formio.service.FormioData(dict=None, /, **kwargs)
Handle formio (submission) data transparently.
Form.io supports component keys in the format ‘topLevel.nested’ which get converted to deep-setting of object properties (using
lodash.setinternally). This datastructure mimicks that interface in Python so we can more naturally perform operations like:data = FormioData() for component in iter_components(...): data[component["key"]] = ...
without having to worry about potential deep assignments or leak implementation details.
Warning
Internally, the data is saved in a nested dictionary structure, which means it is NOT useful to iterate over the values using
FormioData.items(). For nested keys ({“foo.bar”: “baz”}), you will NOT get the complete key if you do this, but only the top-level key “foo” with value {“bar”: “baz”}.Unfortunately, we cannot block
.items()from being used completely, as serializers need to be able to iterate over the data.
- openforms.formio.service.as_json_schema(component: Component, _register: ComponentRegistry | None = None) JSONObject | list[JSONObject] | None
Return a JSON schema of a component.
A description will be added if it is available.
- Layout components require some extra attention:
Content and softRequiredErrors components do not have any values, so a schema does not exist: returns
NoneColumns and fieldset components contain other components for which a JSON schema needs to be generated: returns a list of JSON objects with the child component key as key and the child component JSON schema as value.
- Parameters:
component – Component
_register – Optional component registry to use. If no registry was provided, the default registry will be used.
- Returns:
None for content and softRequiredErrors components, list of JSON objects for columns and fieldsets, and a JSON object otherwise.
- openforms.formio.service.build_serializer(components: Sequence[Component], _register: ComponentRegistry | None = None, **kwargs)
Translate a sequence of Formio.js component definitions into a serializer.
This recursively builds up the serializer fields for each (nested) component and puts them into a serializer instance ready for validation.
- openforms.formio.service.extract_variables_from_template_properties(component: Component) set[str]
Extract all variables used in the template expressions of the relevant properties.
Relevant properties: label, groupLabel, legend, defaultValue, description, html, placeholder, and tooltip.
- Parameters:
component – Component to extract variables from.
- Returns:
Set of variable names, empty if no variables were extracted.
- openforms.formio.service.format_value(component: Component, value: Any, *, as_html: bool = False)
Format a submitted value in a way that is most appropriate for the component type.
- openforms.formio.service.get_dynamic_configuration(config_wrapper: FormioConfigurationWrapper, submission: Submission, data: FormioData | None = None) FormioConfigurationWrapper
Given a static Formio configuration, apply the hooks to dynamically transform this.
The configuration is modified in the context of the provided
submissionparameter.
- openforms.formio.service.get_readable_path_from_configuration_path(configuration: FormioConfiguration, path: str, prefix: str | None = '') str
Get a readable version of the configuration path.
For example, for a path
components.0.components.1and a configuration:{ "components": [ { "key": "repeatingGroup", "label": "Repeating Group", "components": [ { "key": "item1", "label": "Item 1", }, { "key": "item2" "label": "Item 2", } ] } ] }
it returns
Repeating Group > Item 1.
- openforms.formio.service.inject_variables(configuration: FormioConfigurationWrapper, values: FormioData) None
Inject the variable values into the Formio configuration.
Takes a Formio configuration and fully resolved variable state (mapping of variable name to its value as a Python object). The configuration is iterated over and every component is checked for properties that can be templated. Note that the configuration is mutated in the process!
- Parameters:
configuration – A dictionary containing the static Formio configuration (from the form designer)
values – A mapping of variable key to its value (Python native objects)
- Returns:
None - this function mutates the datastructures in place
Todo
Support getting non-string based configuration from variables, such as validate.required etc.
- openforms.formio.service.iterate_data_with_components(configuration: JSONObject, data: FormioData, data_path: str = '', configuration_path: str = 'components', filter_types: list[str] = None) Iterator[ComponentWithDataItem] | None
Iterate through a configuration and return a tuple with the component JSON, its value in the submission data and the path within the submission data.
For example, for a configuration with components:
[ {"key": "surname", "type": "textfield"}, {"key": "pets", "type": "editgrid", "components": [{"key": "name", "type": "textfield"}]} ]
And a submission data:
{"surname": "Doe", "pets": [{"name": "Qui"}, {"name": "Quo"}, {"name": "Qua"}] }
For the “Qui” item of the repeating group this function would yield:
ComponentWithDataItem({"key": "name", "type": "textfield"}, "Qui", "pets", "pets.0.name").
- openforms.formio.service.normalize_value_for_component(component: Component, value: Any) Any
Given a value (actual or default value) and the component, apply the component- specific normalization.
- openforms.formio.service.process_visibility(configuration: FormioConfiguration | Component | Column | JSONObject, data: FormioData, wrapper: FormioConfigurationWrapper, *, data_for_hidden_state: FormioData, parent_hidden: bool = False, get_evaluation_data: GetEvaluationData | None = None, components_to_ignore_hidden: set[str] | None = None, data_for_visible_state: FormioData | None = None) None
Process the visibility of the components inside the configuration, by checking if they were hidden because of conditional logic or a hidden parent, and clearing the value when applicable (
clearOnHideisTrue).Note that the data mutations are applied directly.
- Parameters:
configuration – Configuration-like: can be a Formio configuration, component, or column.
data – Data used for processing.
wrapper – Formio configuration wrapper. Required for component lookup.
data_for_hidden_state – Data to apply when a component is hidden.
parent_hidden – Indicates whether the parent component was hidden. Note that the conditional will not be evaluated at all when this is set to
True.get_evaluation_data – Function used to get the evaluation data used during evaluation of the conditional. If not provided,
datawill be used instead.components_to_ignore_hidden – Set of components for which the “hidden” property is ignored in determining whether the component is hidden. Note that if it was not passed, the hidden property WILL be checked.
data_for_visible_state – The data used to restore values when flipping visibility states.
- openforms.formio.service.rewrite_formio_components(configuration_wrapper: FormioConfigurationWrapper, submission: Submission, data: FormioData | None = None) FormioConfigurationWrapper
Loop over the formio configuration and mutate components in place.
- Parameters:
configuration_wrapper – Container object holding the Formio form configuration to be updated (if applicable). The rewriting loops over all components one-by-one and applies the changes.
submission – The submission instance for which we are rewriting the configurations. This allows you to inspect state and use convenience methods that may not be available in the variables data.
data – key-value mapping of variable name to variable value. If a submission context is available, the variables of the submission are included here.
- openforms.formio.service.rewrite_formio_components_for_request(configuration_wrapper: FormioConfigurationWrapper, request: Request) FormioConfigurationWrapper
Loop over the formio configuration and inject request-specific configuration.
- class openforms.formio.registry.BasePlugin(identifier: str)
Base class for Formio component plugins.
- static apply_visibility(component: ComponentT, data: FormioData, wrapper: FormioConfigurationWrapper, *, data_for_hidden_state: FormioData, parent_hidden: bool, components_to_ignore_hidden: set[str], get_evaluation_data: Callable | None = None, data_for_visible_state: FormioData | None = None) None
Apply (conditional) visibility of this component. This routine should be implemented in the child class.
- Parameters:
component – Component configuration.
data – Data used for processing.
wrapper – Formio configuration wrapper. Required for component lookup.
data_for_hidden_state – Data to apply when a component is hidden.
parent_hidden – Indicates whether the parent component was hidden.
get_evaluation_data – Function used to get the evaluation data used during evaluation of the conditional.
components_to_ignore_hidden – Set of components for which the “hidden” property is ignored in determining whether the component is hidden.
data_for_visible_state – The data used to restore values when flipping visibility states.
- static as_json_schema(component: ComponentT) JSONObject
Return JSON schema for this formio component plugin. This routine should be implemented in the child class
- formatter: type[FormatterProtocol[ComponentT]]
Specify the callable to use for formatting.
Formatter (class) implementation, used by
openforms.formio.registry.ComponentRegistry.format().
- holds_submission_data: ClassVar[bool] = True
Flag to indicate whether data can be submitted for this component.
- normalizer: NormalizerProtocol[ComponentT] | None = None
Specify the normalizer callable to use for value normalization.
- pre_registration_hook: PreRegistrationHookProtocol[ComponentT] | None = None
Hook to perform component-specific registration logic during the “pre-registration” phase.
- rewrite_for_request: RewriterForRequestProtocol[ComponentT] | None = None
Callback to invoke to rewrite plugin configuration for a given HTTP request.
- static test_conditional(component: ComponentT, value: VariableValue, compare_value: VariableValue) bool | None
Perform a component-specific comparison whether a conditional is triggered.
- Parameters:
component – Component to evaluate.
value – Value to evaluate.
compare_value – Value to compare - should be fetched from the conditional in the component configuration.
- Returns:
Whether the conditional was triggered, or
Noneif not overridden in the child class.
Extending
Using our usual extension pattern you can register your own types.
Extensions should inherit from openforms.formio.registry.BasePlugin or
implement the same protocol(s) and be registered with their form.io type:
from openforms.formio.formatters.formio import DefaultFormatter
from openforms.formio.registry import BasePlugin
@register("myCustomType")
class MyComponent(BasePlugin):
formatter = DefaultFormatter
You can find some examples in openforms.formio.components.custom.
Private API
Module: openforms.formio.dynamic_config
Implement component-specific dynamic configuration based on (submission) state.
- openforms.formio.dynamic_config.rewrite_formio_components(configuration_wrapper: FormioConfigurationWrapper, submission: Submission, data: FormioData | None = None) FormioConfigurationWrapper
Loop over the formio configuration and mutate components in place.
- Parameters:
configuration_wrapper – Container object holding the Formio form configuration to be updated (if applicable). The rewriting loops over all components one-by-one and applies the changes.
submission – The submission instance for which we are rewriting the configurations. This allows you to inspect state and use convenience methods that may not be available in the variables data.
data – key-value mapping of variable name to variable value. If a submission context is available, the variables of the submission are included here.
Module: openforms.formio.formatters
Module: openforms.formio.rendering
Implement the bindings with openforms.submissions.rendering.Renderer.
Formio is currently the engine powering the form definitions/configurations which brings along some implementation details. The specifics of rendering Formio configurations together with the data of a submission are contained in this subpackage.
The module openforms.submissions.rendering.nodes contains the (base) node
classes with their public API. For specific Formio component types, you can register
a more specific subclass using the registry. The vanilla Formio components requiring
special attention are implemented in openforms.submissions.rendering.default.