JSON Schema Forms

Nagelfluh uses JSON Schema forms for process parameter configuration and other dynamic form generation. The frontend extends the @rjsf/core library with custom fields and widgets.

Overview

Forms are automatically generated from JSON Schema definitions provided by process types. The custom form system is located in frontend/src/jsoneditor/.

Architecture

jsoneditor/
├── index.js              # Main exports
├── CustomForm.js         # Wrapper around @rjsf Form
├── CustomStringField.js  # Custom string field with format detection
└── DatasetSelector.js    # Dataset selection widget

Basic Usage

Using CustomForm

Always use CustomForm instead of the standard @rjsf Form:

import CustomForm from './jsoneditor';

function ProcessEditor() {
  const [formData, setFormData] = useState({});

  const schema = {
    type: 'object',
    properties: {
      name: {
        type: 'string',
        title: 'Process Name'
      },
      threshold: {
        type: 'number',
        title: 'Threshold',
        default: 0.5
      }
    }
  };

  const handleSubmit = ({ formData }) => {
    console.log('Submitted:', formData);
  };

  return (
    <CustomForm
      schema={schema}
      formData={formData}
      onChange={({ formData }) => setFormData(formData)}
      onSubmit={handleSubmit}
    />
  );
}

Why CustomForm?

CustomForm provides: - Custom field handlers: Detects special formats like x-format: "dataset" - Enhanced widgets: Dataset selector, color picker, etc. - Consistent styling: Bootstrap-based theme - Validation: Built-in JSON Schema validation - Error handling: User-friendly error messages

Dataset Selection

The most important custom feature is the dataset selector for process inputs.

Schema Definition

To enable dataset selection, use these schema properties:

{
  type: 'object',
  properties: {
    input_data: {
      type: 'string',
      format: 'uri',           // Must be 'uri'
      'x-format': 'dataset',   // Triggers custom selector
      title: 'Input Dataset'
    }
  }
}

DatasetSelector Component

The DatasetSelector provides a searchable dropdown for selecting process outputs.

Features: - Debounced search (300ms delay) - Smart grouping: When >4 processes match, shows first dataset + count - Click to refine: Click grouped item to add process name to search - Format: "Process Name / v123 / dataset-name" - Value: Stores full URL: http://localhost:8000/dataset/{id}

Implementation: See frontend/src/jsoneditor/DatasetSelector.js for the complete implementation including: - Debounced search (300ms) - Dataset grouping logic - Loading states - Click handlers

Using Selected Dataset

The form data will contain the dataset URL:

const handleSubmit = ({ formData }) => {
  console.log(formData.input_data);
  // Output: "http://localhost:8000/dataset/abc-123-xyz"

  // Fetch the dataset
  fetch(formData.input_data)
    .then(r => r.json())
    .then(data => {
      // Process dataset
    });
};

Custom Field Detection

CustomStringField automatically detects special formats and renders appropriate widgets.

CustomStringField Logic

CustomStringField detects special format and x-format properties in the schema and renders appropriate widgets.

See: frontend/src/jsoneditor/CustomStringField.js for format detection logic including: - Dataset selector (format: 'uri' + x-format: 'dataset') - Color picker (format: 'color') - Extensible format detection pattern

Adding Custom Formats

To add a new custom format:

  1. Define schema property:
{
  my_field: {
    type: 'string',
    format: 'my-custom-format',
    'x-widget': 'custom',  // Optional additional hint
    title: 'My Field'
  }
}
  1. Add detection in CustomStringField:
if (schema.format === 'my-custom-format') {
  return <MyCustomWidget {...props} />;
}
  1. Create widget component:
function MyCustomWidget({ value, onChange }) {
  return (
    <div>
      <input
        type="text"
        value={value || ''}
        onChange={(e) => onChange(e.target.value)}
      />
      {/* Custom UI */}
    </div>
  );
}

Schema Features

Supported Types

Validation

{
  type: 'string',
  minLength: 3,
  maxLength: 50,
  pattern: '^[a-zA-Z0-9_-]+$',
  title: 'Process Name'
}

Enums (Dropdowns)

{
  type: 'string',
  enum: ['option1', 'option2', 'option3'],
  default: 'option1',
  title: 'Select Option'
}

Arrays

{
  type: 'array',
  items: {
    type: 'string'
  },
  title: 'Tags'
}

Nested Objects

{
  type: 'object',
  properties: {
    solver: {
      type: 'object',
      title: 'Solver Configuration',
      properties: {
        method: {
          type: 'string',
          enum: ['CG', 'LBFGS'],
          title: 'Method'
        },
        tolerance: {
          type: 'number',
          default: 1e-6,
          title: 'Tolerance'
        }
      }
    }
  }
}

Conditional Fields

Show/hide fields based on other field values:

{
  type: 'object',
  properties: {
    enable_feature: {
      type: 'boolean',
      title: 'Enable Feature'
    },
    feature_config: {
      type: 'object',
      title: 'Feature Configuration',
      properties: {
        param1: { type: 'string' }
      }
    }
  },
  dependencies: {
    enable_feature: {
      oneOf: [
        {
          properties: {
            enable_feature: { const: true }
          },
          required: ['feature_config']
        },
        {
          properties: {
            enable_feature: { const: false }
          }
        }
      ]
    }
  }
}

UI Hints

Titles and Descriptions

{
  type: 'number',
  title: 'Regularization Parameter',           // Label
  description: 'Controls smoothness of result', // Help text
  default: 0.01
}

Placeholders

{
  type: 'string',
  title: 'Process Name',
  default: '',
  examples: ['my-process-123']  // Shows as placeholder
}

Widget Hints

{
  type: 'string',
  title: 'Description',
  format: 'textarea',  // Multi-line input
  default: ''
}

Form Validation

Built-in Validation

JSON Schema validation runs automatically:

const schema = {
  type: 'object',
  properties: {
    count: {
      type: 'integer',
      minimum: 1,
      maximum: 100
    }
  },
  required: ['count']
};

<CustomForm
  schema={schema}
  formData={formData}
  onSubmit={handleSubmit}
  onError={(errors) => console.log('Validation errors:', errors)}
/>

Custom Validation

Add custom validation functions:

function validate(formData, errors) {
  if (formData.start_date > formData.end_date) {
    errors.end_date.addError('End date must be after start date');
  }
  return errors;
}

<CustomForm
  schema={schema}
  formData={formData}
  validate={validate}
  onSubmit={handleSubmit}
/>

Live Validation

Enable real-time validation:

<CustomForm
  schema={schema}
  formData={formData}
  liveValidate={true}  // Validate on every change
  onSubmit={handleSubmit}
/>

Styling

Theme Customization

CustomForm uses Bootstrap theme by default:

import { ThemeProvider } from '@rjsf/core';
import { Theme as Bootstrap4Theme } from '@rjsf/bootstrap-4';

<ThemeProvider theme={Bootstrap4Theme}>
  <CustomForm schema={schema} />
</ThemeProvider>

Custom CSS

Target form elements with CSS:

.rjsf .form-group {
  margin-bottom: 15px;
}

.rjsf .field-string input {
  width: 100%;
  padding: 8px;
}

.rjsf .field-description {
  font-size: 0.9em;
  color: #666;
}

Best Practices

Schema Design

✅ DO: Provide defaults and descriptions

{
  type: 'number',
  title: 'Threshold',
  description: 'Values below this will be filtered out',
  default: 0.5,
  minimum: 0,
  maximum: 1
}

❌ DON'T: Use unclear field names

{
  type: 'number',
  title: 'T',  // ❌ Too cryptic
  default: 0.5
}

Form State

✅ DO: Control form data via state

const [formData, setFormData] = useState({});

<CustomForm
  formData={formData}
  onChange={({ formData }) => setFormData(formData)}
/>

❌ DON'T: Use uncontrolled forms for complex scenarios

<CustomForm />  // ❌ No state management

Error Handling

✅ DO: Handle submission errors gracefully

const handleSubmit = async ({ formData }) => {
  try {
    await submitProcess(formData);
  } catch (error) {
    setError(error.message);
  }
};

❌ DON'T: Ignore validation errors

const handleSubmit = ({ formData }) => {
  // ❌ No error handling
  submitProcess(formData);
};

Advanced Topics

Custom Templates

Override field templates for custom layouts:

import { FieldTemplate } from './CustomFieldTemplate';

<CustomForm
  schema={schema}
  FieldTemplate={FieldTemplate}
/>

Custom Widgets

Register custom widgets for specific types:

const widgets = {
  colorPicker: ColorPickerWidget,
  datasetSelector: DatasetSelector
};

<CustomForm
  schema={schema}
  widgets={widgets}
/>

// Use in schema:
{
  type: 'string',
  title: 'Color',
  widget: 'colorPicker'  // References custom widget
}

Form Context

Pass additional data to custom widgets:

const formContext = {
  processTypes: availableProcessTypes,
  currentUser: user
};

<CustomForm
  schema={schema}
  formContext={formContext}
/>

// Access in custom widget:
function MyWidget({ formContext }) {
  const { processTypes } = formContext;
  // ...
}

Dynamic Schemas

Generate schemas dynamically based on conditions:

function ProcessEditor() {
  const [processType, setProcessType] = useState('fft');
  const [schema, setSchema] = useState(null);

  useEffect(() => {
    fetch(`/process-types/${processType}/schema`)
      .then(r => r.json())
      .then(setSchema);
  }, [processType]);

  return schema ? (
    <CustomForm schema={schema} />
  ) : (
    <div>Loading...</div>
  );
}

Reference

@rjsf Documentation

For more details on JSON Schema form features, see: - @rjsf/core documentation - JSON Schema specification

Nagelfluh-Specific Extensions