Skip to Content
CustomizeModel UIDeposit form

Deposit form

The deposit form enables users to create and edit records. It’s a React-based form application that provides field validation, dynamic sections, and integration with your record model.

Request and Application Flow

Request Flow

When a user accesses the deposit form URL:

Application Layers

The deposit form application is organized into server-side and client-side layers:

Server-Side Layers

LayerComponentSourcePurpose
ViewRecordsUIResource.deposit_create/edit()oarepo-ui Handles HTTP request, authenticates user, fetches record/draft data
Componentsrun_components()oarepo-ui Executes component hooks to modify form_config and add context variables
Default Componentoarepo_ui.pages.DepositCreate/Editoarepo-ui Pre-defined JinjaX page component with def block
Model Templatemodel_name/deposit_edit|create.htmlnrp-model-copier Model-specific template extending oarepo-ui base
Base Templateinvenio_app_rdm/records/deposit.htmlinvenio-app-rdm Provides page skeleton (CSS, JS, header, footer blocks)
Form Contentdeposit/form.htmloarepo-ui Renders hidden inputs with config and placeholder div for React app

Client-Side Layers

LayerComponentSourcePurpose
Entry Point{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.jsModel-specific (nrp-model-copier )Webpack entry that mounts React app to #deposit-form div
Form AppDepositFormAppoarepo-ui Main React form component with providers, router, and context
BootstrapDepositBootstrapinvenio-rdm-records Wraps form with notification providers and service setup
Form LayoutBaseFormLayoutoarepo-ui Generic form layout component with overridable fields container
Fields Container{{model_name}}/semantic-ui/js/{{model_name}}/forms/FormFieldsContainer.jsxModel-specific (nrp-model-copier )Your custom component that defines form fields

Configuration

UI Resource Routes

The deposit routes are defined in your model’s UI resource config:

ui/mymodel/__init__.py
from oarepo_ui.resources.records.config import RecordsUIResourceConfig class MymodelUIResourceConfig(RecordsUIResourceConfig): blueprint_name = "mymodel" url_prefix = "/mymodel" routes = { "deposit_create": "/uploads/new", "deposit_edit": "/uploads/<pid_value>", # ... other routes }

Form Configuration

Form field definitions, labels, hints, and validation rules are derived from your record model’s YAML schema definition. See Model schema customization for details on defining field types, labels, hints, and help text.

Template Context

When no custom page template is configured, oarepo-ui uses the default page component (oarepo_ui.pages.DepositCreate or oarepo_ui.pages.DepositEdit). This component declares all context variables in a {# def #} block and extends your model’s base template.

For example, the default component for deposit_create page:

{# def theme, forms_config, searchbar_config, record, community, community_ui, community_use_jinja_header, files, preselectedCommunity=None, files_locked, extra_context, ui_links, permissions, webpack_entry, #} {% extends model_name ~ "/deposit_create.html" %}

The default component extends your model’s base template (mymodel/deposit_create.html or mymodel/deposit_edit.html), which is provided by nrp-model-copier and extends oarepo_ui/base_deposit.html.

These context variables are available in your templates:

VariableDescription
themeTheme configuration
forms_configForm configuration
searchbar_configSearch bar configuration
recordCurrent record data (for edit mode)
communityCommunity data if applicable
community_uiCommunity UI data
filesRecord files entries
extra_contextAdditional context from resource components
ui_linksUI links
permissionsUser permissions
webpack_entryWebpack entry point for the deposit form JavaScript

Building Your Deposit Form

The nrp-model-copier template provides a starting point for your deposit form with only two example fields:

  1. Title - A text field in the “Basic information” section
  2. File uploads - A file uploader in the “Files upload” section

To create a form for your specific model, you typically need to build your own from this starting point.

Define Your Fields in the Model Schema

Add your fields to {{model_name}}/metadata.yaml. Field metadata (labels, hints, required status) is defined here:

model/{{model_name}}/metadata.yaml
# Definition of metadata for {{model_name}} Metadata: properties: title: type: fulltext+keyword label: en: Title cs: Název help: en: The title of the record cs: Název záznamu # Add your custom fields here description: type: fulltext label: en: Description cs: Popis creators: type: array items: type: vocabulary vocabulary-type: creators label: en: Creators cs: Autoři

See Model schema customization for details on defining field types, labels, hints, and help text.

Build Your Form in FormFieldsContainer

Edit ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/FormFieldsContainer.jsx to define your form fields organized into accordion sections:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/FormFieldsContainer.jsx
import * as React from "react"; import { useFormConfig, TextField, ArrayField } from "@js/oarepo_ui/forms"; import { AccordionField } from "react-invenio-forms"; import { i18next } from "@translations/i18next"; import { UppyUploader } from "@js/invenio_rdm_records"; const FormFieldsContainerComponent = ({ record }) => { const formConfig = useFormConfig(); const { filesLocked } = formConfig; return ( <React.Fragment> {/* Basic Information Section */} <AccordionField includesPaths={["metadata.title", "metadata.description"]} active label={i18next.t("Basic information")} > <TextField fieldPath="metadata.title" /> <TextField fieldPath="metadata.description" // Field metadata (label, hint) comes from model schema /> </AccordionField> {/* Creators Section - Array Field */} <AccordionField includesPaths={["metadata.creators"]} active label={i18next.t("Creators")} > <ArrayField fieldPath="metadata.creators" addButtonLabel={i18next.t("Add creator")} > {/* Creator subfields */} </ArrayField> </AccordionField> {/* Files Upload Section */} <AccordionField includesPaths={["files.enabled"]} active label={ <label htmlFor="files.enabled">{i18next.t("Files upload")}</label> } data-testid="filesupload-button" > <UppyUploader isDraftRecord={!record.is_published} config={formConfig} quota={formConfig.quota} filesLocked={filesLocked} /> </AccordionField> </React.Fragment> ); };

Key concepts:

  • AccordionField - Groups related fields into collapsible sections
  • includesPaths - Array of field paths that trigger accordion expansion when modified, and ensures field errors display under the correct section
  • active - Whether section is expanded by default
  • fieldPath - Dot-separated path to the field in the record data (e.g., metadata.title)

Choose Field Components

Use the components documented in Deposit Form Components to add different field types:

ComponentUse forExample
TextFieldSingle-line/multi-line text inputmetadata.title, metadata.name
RichInputFieldRich text editormetadata.description
ArrayFieldArrays of itemsmetadata.creators, metadata.keywords
VocabularyFieldVocabulary-selected itemsmetadata.resource_type
EDTFDateRangePickerFieldDate rangesmetadata.publication_date
SelectFieldDropdown selectionsCustom enumerations

If no suitable field component exists, you can create custom form components.

Verify Entry Point Configuration

The webpack entry point (index.js) is automatically configured by the copier template. It registers your FormFieldsContainer as an override:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
import { DepositFormApp, parseFormAppConfig } from "@js/oarepo_ui/forms"; import ReactDOM from "react-dom"; import { OARepoDepositSerializer } from "@js/oarepo_ui/api"; import FormFieldsContainer from "./FormFieldsContainer"; const overridableIdPrefix = "DepositForm"; const rootElem = document.getElementById("deposit-form"); const config = parseFormAppConfig(rootElem); // Register your custom FormFieldsContainer config.overridableComponents = { [`${overridableIdPrefix}.FormFields.container`]: FormFieldsContainer, }; ReactDOM.render( <DepositFormApp config={config} serializer={OARepoDepositSerializer} />, rootElem, );

You typically don’t need to modify the entry point - just update FormFieldsContainer.jsx with your form fields.

Use the Wizard Form Layout (Optional)

For complex forms with many fields, you can enable a wizard-style layout with vertical tabs. Enable wizard mode by adding the useWizardForm prop and providing sections:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
ReactDOM.render( <DepositFormApp config={config} {...rest} sections={mySections} recordSerializer={OARepoDepositSerializer} componentOverrides={componentOverrides} useWizardForm // Enable wizard layout />, rootEl, );

How the Tab Form Works

  • Free navigation — users can switch between tabs at any time without being required to complete or validate the current section first.
  • Auto-save on tab switch — if the form has unsaved changes when switching tabs, it automatically saves the draft. This also triggers a fresh round of validation errors from the backend, so validation feedback is up to date.
  • URL persistence — the active tab is reflected in the URL via the ?tab=key query parameter, so users can bookmark or share a link that opens directly on a specific section.

Defining Sections

Each section is a plain JavaScript object. The recommended approach is to define each section in its own file, then assemble them into an ordered array. This mirrors how ccmm-invenio organizes its built-in sections.

A single section file looks like this:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/FilesSection.js
import React from "react"; import { i18next } from "@translations/i18next"; import { UppyUploader } from "@js/invenio_rdm_records"; export const FilesSection = { key: "files", label: i18next.t("Upload files"), component: ({ record, formConfig }) => { const { filesLocked } = formConfig.config; return ( <UppyUploader isDraftRecord={!record.is_published} config={formConfig} quota={formConfig.quota} filesLocked={filesLocked} /> ); }, includesPaths: ["files.enabled"], };

Then collect all sections into an array in one place and pass it to DepositFormApp:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/sections.js
import { BasicInformationSection } from "./BasicInformationSection"; import { CreatorsSection } from "./CreatorsSection"; import { FilesSection } from "./FilesSection"; export const mySections = [ BasicInformationSection, CreatorsSection, FilesSection, ];
ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
import { mySections } from "./sections"; ReactDOM.render( <DepositFormApp config={config} sections={mySections} recordSerializer={OARepoDepositSerializer} useWizardForm />, rootEl, );

Section object properties:

PropertyTypeDescription
keystringUnique identifier for the section (used in URL as ?tab=key)
labelstringDisplay label shown in the tab menu
componentfunctionCalled as component({ record, formConfig, activeStep, next, back, initialRecord }), returns JSX
includesPathsstring[]Field paths for error tracking — errors in these fields show indicators on this tab

Props available to component:

PropTypeDescription
recordobjectThe current record/draft data. Use to read existing field values (e.g. record.ui.languages, record.is_published)
formConfigobjectThe form configuration object. Contains formConfig.config.vocabularies, formConfig.config.filesLocked, formConfig.quota, formConfig.overridableIdPrefix, etc.
activeStepnumberZero-based index of the currently active section tab
nextfunctionCall next() to programmatically advance to the next tab
backfunctionCall back() to programmatically navigate to the previous tab
initialRecordobjectThe record data as it was when the form first loaded, before any user edits. Useful for comparing current state to the saved state

Using CCMM sections

If during model creation you chose ccmm-invenio, your form will already be provided with prepared sections  for this model.

Adding Custom Sections
ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
import { CCMMSections } from "@js/ccmm_invenio/forms"; import { TextField } from "@js/oarepo_ui/forms"; import { i18next } from "@translations/i18next"; // Start with the shared sections and add your own at the end export const mySections = [...CCMMSections, MyNewSection];
Adding Content to Every Section via Override

To add inputs or content at the bottom of every section (or specific sections), use the Overridable pattern with the section key:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
import { Overridable } from "react-overridable"; // Override for a specific section const BasicInfoExtra = ({ section, activeStep }) => ( <div className="ui segment"> <p>Additional content for {section.label}</p> </div> ); const componentOverrides = { [`${overridableIdPrefix}.FormFields.container`]: FormFieldsContainer, // Add content below the "general-information" section [`${overridableIdPrefix}.TabForm.TabContent.general-information`]: BasicInfoExtra, };

The override component receives these props:

  • section - The current section object
  • activeStep - Current step index
  • next - Function to navigate to next step
  • back - Function to navigate to previous step
Reorganizing CCMM Sections

The CCMMSections export is a fixed ordered array. If you need a different order, or want to omit some sections, you cannot simply sort or filter the imported array — you need to import the individual section objects and assemble your own array:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/sections.js
import { CCMMFiles, CCMMGeneralInformation, CCMMFunding, CCMMAlternativeIdentifiers, CCMMRelatedWorks, CCMMCommunityAndAccess, } from "@js/ccmm_invenio/forms"; import { MyCustomSection } from "./MyCustomSection"; // Re-order, omit, or insert sections as needed export const mySections = [ CCMMGeneralInformation, // moved to first CCMMFiles, MyCustomSection, // inserted between built-in sections CCMMFunding, CCMMRelatedWorks, // CCMMAlternativeIdentifiers omitted entirely // CCMMCommunityAndAccess omitted entirely ];
Overriding Individual Fields Within a Section

Each field inside a CCMM section is wrapped in an <Overridable> component, which means you can replace or adjust it via componentOverrides in your entry point without touching the section file itself.

The overridable ID for a field follows the pattern {overridableIdPrefix}.FieldName — for example, {overridableIdPrefix}.Files for the file uploader in CCMMFiles. The prefix comes from config.overridableIdPrefix returned by parseFormAppConfig().

Hiding a field — return null from the override to remove it from the UI entirely:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
const { rootEl, config, ...rest } = parseFormAppConfig(); const overridablePrefix = config.overridableIdPrefix; const componentOverrides = { // Remove the file uploader section content completely [`${overridablePrefix}.Files`]: () => null, // Remove the version field from General Information [`${overridablePrefix}.Version`]: () => null, };

Modifying field props — render the original component with adjusted props. The override receives the full tabConfig spread (i.e. record, formConfig, activeStep, next, back, initialRecord) as props:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
import { UppyUploader } from "@js/invenio_rdm_records"; const { rootEl, config, ...rest } = parseFormAppConfig(); const overridablePrefix = config.overridableIdPrefix; const componentOverrides = { // Replace the file uploader with a version that locks files [`${overridablePrefix}.Files`]: ({ record, formConfig }) => ( <UppyUploader isDraftRecord={!record.is_published} config={formConfig} quota={formConfig.quota} decimalSizeDisplay={formConfig.decimal_size_display} allowEmptyFiles={formConfig.allow_empty_files} fileUploadConcurrency={formConfig.file_upload_concurrency} showMetadataOnlyToggle={false} filesLocked={true} // override: always lock files /> ), };

Pass componentOverrides to DepositFormApp:

ReactDOM.render( <DepositFormApp config={config} {...rest} sections={mySections} recordSerializer={OARepoDepositSerializer} componentOverrides={componentOverrides} useWizardForm />, rootEl, );

These are front-end only changes. If you are running a ccmm-invenio model, it also has a backend part that expects certain data to be submitted from the form. Removing a field from the UI that is required by ccmm-invenio means there will be no way to fill in that value, and the record cannot be published. Similarly, if you add extra sections with new fields, those fields must also be declared in the backend model schema (e.g. datasets/metadata.yaml).

useFieldData Helper

The useFieldData hook from oarepo_ui/forms provides access to field metadata (labels, help text, hints, required status, icon) defined in your model’s YAML schema. This metadata is extracted during model compilation and made available to React components.

Usage

import { useFieldData } from "@js/oarepo_ui/forms"; const MyField = ({ fieldPath }) => { const { getFieldData } = useFieldData(); const { helpText, label, placeholder, required } = getFieldData({ fieldPath: fieldPath, fieldRepresentation: "text", // "full", "compact", or "text" icon: "pencil", // optional icon }); return ( <div> {/* label is a React node in "full"/"compact" modes or string in "text" mode */} <label>{label}</label> <input name={fieldPath} placeholder={placeholder} /> {helpText && <small>{helpText}</small>} </div> ); };

getFieldData Options

OptionTypeDefaultDescription
fieldPathstring(required)The path to the field in the metadata
fieldRepresentationstring"full"How to render the label: "full", "compact", or "text"
iconstring"pencil"Icon to display with the field
fullLabelClassNamestring-CSS class for full mode label
compactLabelClassNamestring-CSS class for compact mode label
fieldPathPrefixstring(auto-set)Prefix for nested fields, configured by provider
ignorePrefixbooleanfalseWhether to ignore the prefix

Return Values by Representation Mode

ModelabelhelpTextOther properties
fullReact <FieldLabel> componenthelpText stringplaceholder, required, detail
compactReact <CompactFieldLabel> with popup help(in popup)placeholder, required, detail
textPlain stringhelpText stringlabelIcon, placeholder, required, detail

All modes return an object with helpText, label, placeholder, required, detail (plus labelIcon in text mode).

Create Custom Form Components

If the available form components don’t meet your needs, you can create custom fields using these patterns:

  • Wrapping react-invenio-forms components - Use when extending standard form fields
  • Building from scratch with Formik - Use for completely custom UI not based on react-invenio-forms

Pattern 1: Wrapping Base Components

Here’s an example of creating an UppercaseField by wrapping the standard TextField from react-invenio-forms and adding custom behavior:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/UppercaseField.jsx
import React from "react"; import { TextField as InvenioTextField } from "react-invenio-forms"; import { useFieldData } from "@js/oarepo_ui/forms"; import { getIn, useFormikContext } from "formik"; export const UppercaseField = ({ fieldPath, fieldRepresentation = "full", icon = "arrow up", ...rest }) => { const { setFieldTouched, setFieldValue, values } = useFormikContext(); const { getFieldData } = useFieldData(); return ( <InvenioTextField optimized fieldPath={fieldPath} {...getFieldData({ fieldPath, fieldRepresentation, icon })} onBlur={() => { const currentValue = getIn(values, fieldPath); // Convert to uppercase on field blur if (typeof currentValue === "string") { setFieldValue(fieldPath, currentValue.toUpperCase()); } setFieldTouched(fieldPath, true); }} {...rest} /> ); };

Key elements:

ElementPurpose
InvenioTextFieldBase component from react-invenio-forms with Formik integration
optimizedUses FastField for better performance - only re-renders when this field changes
getFieldData()Gets label, helpText, placeholder, required, detail from model YAML
useFormikContext()Access setFieldValue, setFieldTouched, and form values for custom blur behavior
getIn()Safely access nested field values from form state

Pattern 2: Building from Scratch with Formik

For custom UI not based on react-invenio-forms components, use Formik directly (same pattern as react-invenio-forms TextField ):

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/CustomField.jsx
import React from "react"; import { FastField, Field } from "formik"; import PropTypes from "prop-types"; import { Form, Popup } from "semantic-ui-react"; import { useFieldData } from "@js/oarepo_ui/forms"; const CustomField = ({ fieldPath, fieldRepresentation = "text", icon = "pencil", disabled = false, optimized = true, // Props from getFieldData can be overridden here label, helpText, required, placeholder, ...uiProps }) => { const { getFieldData } = useFieldData(); // Get field metadata from model YAML, with props as overrides const fieldData = getFieldData({ fieldPath, fieldRepresentation, icon }); const computedLabel = label ?? fieldData.label; const computedHelpText = helpText ?? fieldData.helpText; const computedRequired = required ?? fieldData.required; const computedPlaceholder = placeholder ?? fieldData.placeholder; const FormikField = optimized ? FastField : Field; return ( <> <FormikField name={fieldPath}> {({ field, meta }) => { // Resolve error to display const computedError = meta.error || (!meta.touched && meta.initialError); let formInputError = null; if (typeof computedError === "string") { formInputError = computedError; } else if ( typeof computedError === "object" && computedError.message ) { // Error object with severity/message - show as popup formInputError = ( <Popup trigger={<span>{computedError.message}</span>} content={computedError.description} position="top center" /> ); } return ( <Form.Input {...field} error={formInputError} disabled={disabled} fluid label={computedLabel} required={computedRequired} placeholder={computedPlaceholder} icon={icon} id={fieldPath} {...uiProps} /> ); }} </FormikField> {/* Optional help text below field */} {computedHelpText && ( <label className="helptext">{computedHelpText}</label> )} </> ); }; CustomField.propTypes = { fieldPath: PropTypes.string.isRequired, fieldRepresentation: PropTypes.string, icon: PropTypes.string, disabled: PropTypes.bool, optimized: PropTypes.bool, label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), helpText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), required: PropTypes.bool, placeholder: PropTypes.string, }; export default CustomField;

Key elements explained:

ElementPurpose
FastField / FieldFormik component for form state. Use FastField (optimized mode) for better performance - only re-renders when this specific field changes
optimized propWhen true, uses FastField. Set to false only if your field depends on other form values
getFieldData()Retrieves label, helpText, placeholder, required, detail from your model’s YAML schema definition
field propSpread Formik’s field props (value, onChange, onBlur, name) onto input
meta propAccess touched, error, initialError for validation display
computedErrorCombines Formik error with optional external error prop
Error handlingSupports both string errors and structured objects with severity/message/description
Props overridelabel, helpText, required, placeholder props override values from getFieldData()

Using Custom Fields

Import and use your custom fields in FormFieldsContainer.jsx:

import { CustomField } from "./CustomField"; import { UppercaseField } from "./UppercaseField"; const FormFieldsContainerComponent = ({ record }) => { return ( <React.Fragment> <AccordionField includesPaths={["metadata.custom"]} active label="Custom"> <CustomField fieldPath="metadata.custom" icon="tag" fieldRepresentation="full" // Override label from schema if needed label="Custom Field Label" /> {/* Field that automatically converts input to uppercase */} <UppercaseField fieldPath="metadata.code" fieldRepresentation="compact" /> </AccordionField> </React.Fragment> ); };

Optional: Custom Page Templates

Add a submission deadline banner that appears when a deadline is approaching:

Step 1: Create the JinjaX component

ui/mymodel/templates/semantic-ui/mymodel/DepositCreate.jinja
{# def record, extra_context, model_name, ... #} {% extends model_name ~ "/deposit_create.html" %}

Step 2: Create the template

ui/mymodel/templates/semantic-ui/mymodel/deposit_create.html
{% extends "oarepo_ui/deposit_create.html" %} {% block page_body %} {% if extra_context.deadline %} <div class="ui message warning"> <i class="icon clock"></i> {{ _("Submission deadline:") }} {{ extra_context.deadline }} </div> {% endif %} {{ super() }} {% endblock %}

Step 2: Register the template

Register in your UI resource config using dot notation:

ui/mymodel/__init__.py
class MymodelUIResourceConfig(RecordsUIResourceConfig): # ... templates = { "deposit_create": "mymodel.DepositCreate", "deposit_edit": "mymodel.DepositEdit", }

Note the dot notation: mymodel.DepositCreate refers to the JinjaX component at mymodel/DepositCreate.jinja.

JinjaX Component Required: When using templates configuration, you must create a valid JinjaX page component (e.g., mymodel/DepositCreate.jinja) with a {# def #} block declaring all context variables. The component should extend your model’s base template. Without the {# def #} block, the template will fail to render.

Custom JavaScript

Add model-specific JavaScript to the deposit form without overriding the entire template:

Create ui/mymodel/templates/semantic-ui/mymodel/deposit_create/javascript.html:

ui/mymodel/templates/semantic-ui/mymodel/deposit_create/javascript.html
<script> // Custom validation before form submission document.addEventListener('DOMContentLoaded', function() { console.log('Custom deposit form behavior'); }); </script>

The base template automatically includes this file if it exists at {model_name}/deposit_create/javascript.html.

Optional: Resource Components

Use resource component hooks to modify form configuration or add template context:

ui/{{model_name}}/components.py
from oarepo_ui.resources.components import UIResourceComponent class ExtraContextComponent(UIResourceComponent): def before_ui_edit(self, resource, request, extra_context, **kwargs): """Add custom context to edit page.""" record = record_from_resource(request) extra_context["record_history"] = self._get_history(record)

Register in your UI resource config:

class MymodelUIResourceConfig(RecordsUIResourceConfig): # ... components = [ExtraContextComponent]

Further Reading

Last updated on