Adding your own components and validator¶
The package exposes three extension points:
validator.js— your client-side Yup validation schema.componentsRegistry.js— extra or replacement React components you reference by name fromMODULAR_DEPOSIT_FORM_COMMON_FIELDS/MODULAR_DEPOSIT_FORM_FIELDS_BY_TYPE.transformations.js— pure functions that rewrite Formik values immediately before submit.
Each is independently optional: if you don’t register one, the package falls back to a no-op stub. See Registering your extension files for the steps.
What goes in each extension file¶
Each file is expected to export a specific kind of customization object:
File |
Required export |
Notes |
|---|---|---|
|
A Yup schema, or a function |
Resolved as |
|
A named export |
Merged onto the built-in registry via |
|
A named export |
Each function is |
See Validation for more on how the schema reaches Formik, and Replacement field components for the touched-aware widgets you should usually compose with when writing replacements.
Registering your extension files¶
The package finds your validator.js, componentsRegistry.js, and transformations.js through Python entry points. Setting that up takes three small steps, done once per instance. You only need to register the files you actually provide — omit any of the three and the package falls back to a no-op default for that one.
1. Make sure your assets folder has a webpack alias¶
In your instance’s site/<my_instance>/webpack.py, add an alias for the directory holding your JS assets (skip this if you already have one for your instance code):
themes={
"semantic-ui": dict(
entry={ ... },
aliases={
"@js/my_instance_name": "js/my_instance_name",
},
),
},
Ensure your theme bundle is loaded — usually via the invenio_assets.webpack entry point.
2. Place your extension files under that folder¶
Put each file you want to register somewhere under your assets directory, e.g.:
site/my_instance_name/assets/semantic-ui/js/my_instance_name/deposit_extras/
├── validator.js
├── componentsRegistry.js
└── transformations.js
3. Tell the package where they are¶
The package reads three Python entry points. Each one points to a Python function that returns the path to the corresponding file, in the form js/<your-alias-name>/path/to/file.js — the same path you’d write after @js/ in a JavaScript import, but with js/ instead of @js/.
Add a small Python module — e.g. site/my_instance_name/deposit_extras.py:
def get_validator_path():
return "js/my_instance_name/deposit_extras/validator.js"
def get_components_registry_path():
return "js/my_instance_name/deposit_extras/componentsRegistry.js"
def get_transformations_path():
return "js/my_instance_name/deposit_extras/transformations.js"
Then register those functions on the entry points. In pyproject.toml:
[project.entry-points."invenio_modular_deposit_form.validator"]
my_instance = "my_instance_name.deposit_extras:get_validator_path"
[project.entry-points."invenio_modular_deposit_form.components_registry"]
my_instance = "my_instance_name.deposit_extras:get_components_registry_path"
[project.entry-points."invenio_modular_deposit_form.transformations"]
my_instance = "my_instance_name.deposit_extras:get_transformations_path"
Or the equivalent in setup.cfg:
[options.entry_points]
invenio_modular_deposit_form.validator =
my_instance = my_instance_name.deposit_extras:get_validator_path
invenio_modular_deposit_form.components_registry =
my_instance = my_instance_name.deposit_extras:get_components_registry_path
invenio_modular_deposit_form.transformations =
my_instance = my_instance_name.deposit_extras:get_transformations_path
After changing entry points, reinstall your instance package (uv pip install -e . or equivalent) and rebuild assets (invenio webpack build) so the new files are picked up.
Warning
Return a path that starts with js/, not @js/. The returned string is resolved against the merged build directory, so @js/my_instance/... would look for a literal @js folder and fail at build time. If your build complains with “Cannot find module … for matched aliased key @js/invenio_modular_deposit_form_validator”, a stray @js/ prefix in one of these functions is almost always the cause.
For reference, the three entry point groups are:
Entry point group |
File |
Purpose |
|---|---|---|
|
|
Client-side Yup schema or schema-builder function. |
|
|
Extra or overriding React components. |
|
|
Pure functions that rewrite Formik values immediately before submit. |
See What goes in each extension file for the exact export each file must provide.
The componentsRegistry object¶
componentsRegistry.js exports a named componentsRegistry whose keys are the strings you reference in your layout config and whose values are [Component, fieldPaths] tuples:
import { FieldComponentWrapper } from "@js/invenio_modular_deposit_form/field_components/FieldComponentWrapper";
import { MyTitlesWrapper } from "./components/MyTitlesWrapper";
import { BookSectionVolumePagesComponent } from "./components/BookSectionVolumePagesComponent";
export const componentsRegistry = {
// Replace the built-in titles section with your own wrapper.
TitlesComponent: [
MyTitlesWrapper,
["metadata.title", "metadata.additional_titles"],
],
// Add a brand-new section for a compound custom field.
BookSectionVolumePagesComponent: [
BookSectionVolumePagesComponent,
[
"custom_fields.journal:journal.pages",
"custom_fields.imprint:imprint.pages",
],
],
};
[0]is the React component the layout will mount whenever it sees that name in"component": "...".[1]is the list of dot-separated metadata field paths the component is responsible for. The form uses this list to map server- and client-side validation errors back to the correct section and to compose the per-section summary inFormFeedback. If your component owns no metadata fields (for example, layout, navigation, or informational widgets), pass[].
Because your registry is merged onto the built-in registry with Object.assign, a key that matches a built-in name (e.g. TitlesComponent) replaces the built-in entry, while a new key is added alongside the built-ins. Replacing a built-in via the registry is the right tool when you need to change which metadata fields a section owns or how its props are assembled; for purely visual replacements of a section’s inner widget, prefer the Overridable API (see Customizing field components).
Using your component in the layout¶
Once a key is in the merged registry, you reference it by that string from MODULAR_DEPOSIT_FORM_COMMON_FIELDS (or any per-resource-type override in MODULAR_DEPOSIT_FORM_FIELDS_BY_TYPE):
{
"section": "discipline",
"label": "Discipline",
"component": "MyDisciplineComponent",
}
There is no separate “register-this-component-with-the-layout” step — the registry key is the layout name.
Customizing field components¶
There are two ways to change how an existing form section is rendered:
You want to… |
Use |
How |
|---|---|---|
Replace the whole section wrapper and control how its props are assembled (different data sources, different field paths, different layout) |
Component registry |
Register your component in |
Add a section for a metadata field with no built-in component |
Component registry |
Add a new key plus |
Replace only the inner widget for a section that already has a working wrapper |
Overridable |
Register your component in your instance’s |
Use the registry when you need to control how props are gathered or when the section composition itself has to change. Use Overridable when the existing wrapper does what you want and you only need a different inner widget.
Imports you typically need when writing a replacement:
import { FieldComponentWrapper } from "@js/invenio_modular_deposit_form/field_components/FieldComponentWrapper";
import {
TextField,
SelectField,
RemoteSelectField,
} from "@js/invenio_modular_deposit_form/replacement_components";
Prefer the package’s replacement field components (TextField, SelectField, RemoteSelectField, etc.) over the stock react-invenio-forms widgets so visible-error gating (“touched”) stays consistent across the form.
Custom layout components¶
The structural components used by the layout config — FormPage, FormSection, FormRow, FormHeader, FormLeftSidebar, FormRightSidebar, FormFooter, FormStepper, etc. — are themselves entries in the component registry. To replace one (or to add a new structural component) you register it in your instance’s componentsRegistry.js under the same name and reference it from the layout config.
When writing a custom layout component:
Forward
childrenunchanged so child sections render inside your layout. The package builds the section tree from the layout config and passes the resolved child elements down.If your component needs to react to the current resource type, current page, or overall error state, read from the package’s
FormUIStateContext/useFormUIStatehook (see Available hooks and contexts below). Doing this through the context, rather than re-deriving the same state from Redux or Formik directly, keeps your component in sync with the rest of the form.For HTML-only changes to a structural component, prefer overriding it via the React Overridable mechanism in your instance’s
mapping.js; that avoids re-implementing prop assembly.
Available hooks and contexts¶
When writing a custom component, these are the imports the package treats as part of its public surface for extenders:
Import |
Purpose |
|---|---|
|
Wrap a custom field widget so it receives the layout’s label/icon/placeholder/help-text mods and exposes the matching Overridable slot. |
|
Render an InvenioRDM custom field by name, resolving its widget and props from |
|
Read the |
|
Resolve the section list for the currently selected resource type (with |
|
Read or control the current form page in a multi-page layout. |
|
The lookup hook used internally by |
|
Current resource type, current page, and combined client + server error state. Use this in custom layout components instead of re-implementing the same derivations. |
Replacement input widgets (TextField, SelectField, RemoteSelectField, TextArea, MultiInput, Input, Dropdown, AutocompleteDropdown) live under @js/invenio_modular_deposit_form/replacement_components — see Replacement field components for the behavioural differences from upstream.
Handling custom fields¶
Custom field values are stored under custom_fields in the record and must be accessed via the correct field path (e.g. custom_fields.kcr:my_field). To implement your own custom field widgets while reusing the standard InvenioRDM custom field configuration, use the CustomField component.
Prerequisites¶
Enable custom fields for every component you use. The form looks up each custom field’s widget and props from RDM_CUSTOM_FIELDS_UI. You must define the field in RDM_CUSTOM_FIELDS and add a section (or add the field to an existing section) in RDM_CUSTOM_FIELDS_UI for every custom field component in your layout—including the built-in ones (journal, imprint, meeting, thesis, codemeta/software). If you use JournalTitleComponent, BookTitleComponent, MeetingTitleComponent, etc., the corresponding sections and fields must be present in your instance’s RDM_CUSTOM_FIELDS_UI; otherwise the components cannot resolve their widgets.
Enable custom fields for every component you use. The form looks up each custom field’s widget and props from RDM_CUSTOM_FIELDS_UI. You must define the field in RDM_CUSTOM_FIELDS and add a section (or add the field to an existing section) in RDM_CUSTOM_FIELDS_UI for every custom field component in your layout—including the built-in ones (journal, imprint, meeting, thesis, codemeta/software). If you use JournalTitleComponent, BookTitleComponent, MeetingTitleComponent, etc., the corresponding sections and fields must be present in your instance’s RDM_CUSTOM_FIELDS_UI; otherwise the components cannot resolve their widgets. The form finds the field config by field name only (e.g. thesis:thesis.university), so section structure does not affect lookup.
For the single-field custom components that this package provides for the built-in contrib fields (journal, imprint, meeting, codemeta), you can either write your own RDM_CUSTOM_FIELDS_UI entries or reuse ready-made helpers shipped with this extension:
from invenio_modular_deposit_form.custom_fields.ui.journal_fields import (
JOURNAL_CUSTOM_FIELDS_UI,
)
from invenio_modular_deposit_form.custom_fields.ui.imprint_fields import (
IMPRINT_CUSTOM_FIELDS_UI,
)
from invenio_modular_deposit_form.custom_fields.ui.meeting_fields import (
MEETING_CUSTOM_FIELDS_UI,
)
from invenio_modular_deposit_form.custom_fields.ui.codemeta_fields import (
CODEMETA_CUSTOM_FIELDS_UI,
)
RDM_CUSTOM_FIELDS_UI = [
# ... any other sections ...,
JOURNAL_CUSTOM_FIELDS_UI,
IMPRINT_CUSTOM_FIELDS_UI,
MEETING_CUSTOM_FIELDS_UI,
CODEMETA_CUSTOM_FIELDS_UI,
]
Each of these helpers defines separate entries for each subfield (for example journal:journal.title, journal:journal.volume, etc.), matching the field names used by the corresponding single-field components (JournalTitleComponent, JournalVolumeComponent, BookTitleComponent, MeetingTitleComponent, CodeRepositoryComponent, and so on). Including them in your RDM_CUSTOM_FIELDS_UI is what allows those components to resolve their widgets automatically via CustomField.
Using CustomField¶
CustomField resolves the field’s widget and props from the custom field UI configuration that is part of the regular InvenioRDM custom fields system. That configuration is defined in your instance’s RDM_CUSTOM_FIELDS_UI (in invenio.cfg or your config module). It is serialized into the deposit form’s config as config.custom_fields.ui: a list of sections, each with a section label and a fields array. Each field entry includes field (e.g. thesis:thesis.university), ui_widget, and props. CustomField looks up the config by field name (searching across all sections), loads the widget, and merges props.
Define your custom field and its UI in InvenioRDM config — Register the field in
RDM_CUSTOM_FIELDSand add a section (or add the field to an existing section) inRDM_CUSTOM_FIELDS_UIwithfield,ui_widget, andpropsas usual.Implement a widget component that uses CustomField — Render
CustomFieldwith:fieldName— thefieldvalue from your UI entry (e.g.kcr:my_fieldorthesis:thesis.university)idString— a stable id for the wrapper (e.g."MyField")Any extra props you want to override or add. These are merged over the config props.
Register the component and add it to the layout — Add your component to your instance’s
componentsRegistry.jswith the field path(s) it handles, then reference it inMODULAR_DEPOSIT_FORM_COMMON_FIELDSorMODULAR_DEPOSIT_FORM_FIELDS_BY_TYPElike any other field component.
Example — single custom field in the instance registry:
import { CustomField } from "@js/invenio_modular_deposit_form/field_components/CustomField";
const MyFieldComponent = (props) => (
<CustomField fieldName="kcr:my_field" idString="MyField" {...props} />
);
// In componentsRegistry.js:
// MyFieldComponent: [MyFieldComponent, ["custom_fields.kcr:my_field"]]
Your instance must define the field and its UI in RDM_CUSTOM_FIELDS and RDM_CUSTOM_FIELDS_UI as usual.
CustomField uses the useCustomFieldWidget hook, which reads custom_fields.ui from the deposit config, finds the field by name (across all sections), merges props (without mutating config), and loads the widget via the same template loaders used elsewhere. The loaded widget is then wrapped in FieldComponentWrapper so it receives resource-type-driven label, labelIcon (and legacy icon folded into it), and other mods from the layout config.