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 Template`model_name/deposit_editcreate.html`nrp-model-copier
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.

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