Volume 5: Domain Template Creation

Chapter 4.6: Automatic Form Generation — Zero-Code Report Parameters

"You define your table structure once. The platform generates forms, validates input, and builds filter queries automatically. 20 templates means 20 parameter forms — all built for you."


The Problem Form Generation Solves

A Class Roster template needs to be filtered. Maybe the administrator wants only Grade 3 students. Maybe they want a specific co-op. Maybe they want a particular academic term. Maybe all three.

Without form generation, you would need to:

  1. Create an HTML form manually for each template
  2. Add input fields for each possible filter
  3. Write validation logic in JavaScript
  4. Handle form submission and parameter passing
  5. Parse parameters on the server
  6. Build WHERE clauses from the parsed data

Multiply that by 20 templates and you have 120 steps just to make filtering work. And when you add a new field to your table definition, you have to manually update every form that filters on that table.

The Data Publisher platform eliminates this entire category of work through automatic form generation. You define your table structure in domain-config.json. The platform generates the forms, validates the inputs, passes parameters correctly, and builds filter queries — all automatically.

This chapter shows you how to design your table definitions to enable rich, user-friendly report parameter forms with zero additional code.


How It Works: From Table Definition to Working Form

When a user clicks "Generate" on a Class Roster template card in the Reports tab, the platform:

  1. Looks up the template in documentTypes to find its primaryTable and relatedTables
  2. Scans the table definitions to find all fields that could be useful filters
  3. Renders a dialog with dropdowns, checkboxs, date pickers, and text inputs based on field types
  4. Populates dropdown options by querying actual data from the CSV (or database if connected)
  5. Validates user selections client-side using the field validation rules from domain-config.json
  6. Passes parameters to the server as a structured object: {filters: {CoopID: "1", TermID: "3"}, sort: {field: "ClassName", direction: "asc"}}
  7. Builds SQL WHERE clauses (or CSV filter logic) from the parameters automatically
  8. Returns filtered data to the document merge engine

You wrote none of this code. You only defined your table structure correctly in domain-config.json.


The Magic is in Field Metadata

Every field in your table definition can include metadata that controls how it appears in generated forms:

{
  "name": "CoopID",
  "type": "lookup",
  "required": true,
  "references": "coops",
  "displayField": "CoopName",
  "description": "The co-op organization",
  "filterable": true,
  "defaultToFirst": true
}

Let's break down what each property does for form generation:

type — Determines Input Control

The field type maps directly to HTML input controls in generated forms:

Type Generated Control Example Use Case
text <input type="text"> Names, addresses, notes
number <input type="number"> Ages, counts, quantities
currency <input type="number" step="0.01"> Prices, fees, balances
date <input type="date"> Birth dates, deadlines
datetime <input type="datetime-local"> Timestamps, appointments
select <select> with hardcoded options T-shirt sizes, states, grades
lookup <select> with dynamic options Foreign keys to other tables
boolean <input type="checkbox"> Yes/no flags
email <input type="email"> Email addresses
phone <input type="tel"> Phone numbers

When the form generator encounters type: "date", it renders an HTML5 date picker with calendar popup. When it sees type: "lookup", it queries the referenced table and builds a dropdown.

references and displayField — Smart Dropdowns

Lookup fields create the most user-friendly form controls:

{
  "name": "ClassID",
  "type": "lookup",
  "references": "classes",
  "displayField": "ClassName"
}

This tells the platform:

  • references: Query the classes table to get options
  • displayField: Show users the ClassName field, not the ID
  • Behind the scenes, pass the ClassID value when building filters

The generated dropdown looks like:

┌─────────────────────────────────┐
│ Select a class...              │
│ ─────────────────────────────  │
│ Math 101                       │  ← User sees "Math 101"
│ Science Lab                    │     but system stores ClassID=5
│ Literature                     │
│ Art & Music                    │
└─────────────────────────────────┘

Users navigate by human-readable names. The system tracks by foreign key IDs. No manual option mapping required.

required — Validation Rules

{
  "name": "CoopID",
  "type": "lookup",
  "required": true,
  "references": "coops",
  "displayField": "CoopName"
}

When required: true, the generated form:

  • Shows a red asterisk next to the field label
  • Prevents form submission if the field is empty
  • Displays validation error: "Co-op is required" if user tries to submit

When required: false:

  • Field is optional
  • Renders with lighter label text
  • Form submits even if field is blank

filterable — Inclusion in Parameter Forms

Not every field makes sense as a filter:

{
  "name": "InternalNotes",
  "type": "text",
  "required": false,
  "filterable": false,
  "description": "Staff use only"
}

Setting filterable: false excludes this field from report parameter forms entirely. It still exists in the table, it's still available in document merges, but users won't see it as a filter option when generating reports.

By default, foreign keys (type: "lookup") and common filter fields (like dates, dropdowns) are assumed filterable. Text fields with names like "Notes" or "Comments" are assumed not filterable.

defaultToFirst — Smart Defaults

When you have a filter that most users will fill out the same way every time:

{
  "name": "CoopID",
  "type": "lookup",
  "required": true,
  "references": "coops",
  "displayField": "CoopName",
  "defaultToFirst": true
}

The form will pre-select the first option in the dropdown automatically. If there's only one co-op in the system, the field is already filled out when the dialog opens. Users just click Run.


Real-World Example: Class Roster Parameters

Here's how a Class Roster template's filter form is generated from table metadata:

Template Definition

{
  "id": 1,
  "name": "Class Roster",
  "filename": "Template-01-Class-Roster.docx",
  "category": "Academic",
  "primaryTable": "classes",
  "relatedTables": ["enrollments", "students", "families", "teachers", "academic_terms"]
}

Relevant Table Field Definitions

// From the students table
{
  "name": "CoopID",
  "type": "lookup",
  "required": true,
  "references": "coops",
  "displayField": "CoopName",
  "filterable": true,
  "defaultToFirst": true
}

// From the classes table  
{
  "name": "TermID",
  "type": "lookup",
  "required": true,
  "references": "academic_terms",
  "displayField": "TermName",
  "filterable": true
}

{
  "name": "ClassID",
  "type": "lookup",
  "required": true,
  "references": "classes",
  "displayField": "ClassName",
  "filterable": true
}

{
  "name": "TeacherID",
  "type": "lookup",
  "required": false,
  "references": "teachers",
  "displayField": "FirstName,LastName",
  "filterable": true
}

Generated Parameter Form

The platform automatically creates a dialog with:

┌──────────────────────────────────────────────┐
│  Class Roster Parameters                     │
├──────────────────────────────────────────────┤
│                                               │
│  Co-op Organization *                        │
│  ┌────────────────────────────────────────┐  │
│  │ Westside Homeschool Co-op       ▾     │  │ ← Pre-selected (defaultToFirst)
│  └────────────────────────────────────────┘  │
│                                               │
│  Academic Term *                             │
│  ┌────────────────────────────────────────┐  │
│  │ Select a term...                 ▾     │  │
│  │ ─────────────────────────────────────  │  │
│  │ Fall 2025 (2025-2026)                  │  │
│  │ Spring 2025 (2024-2025)                │  │
│  └────────────────────────────────────────┘  │
│                                               │
│  Class                                       │
│  ┌────────────────────────────────────────┐  │
│  │ All Classes                      ▾     │  │ ← Optional (required: false)
│  │ ─────────────────────────────────────  │  │
│  │ Math 101                               │  │
│  │ Science Lab                            │  │
│  │ Literature                             │  │
│  └────────────────────────────────────────┘  │
│                                               │
│  Teacher                                     │
│  ┌────────────────────────────────────────┐  │
│  │ All Teachers                     ▾     │  │
│  │ ─────────────────────────────────────  │  │
│  │ Sarah Johnson                          │  │
│  │ Michael Martinez                       │  │
│  └────────────────────────────────────────┘  │
│                                               │
│  Sort By                                     │
│  ┌────────────────────────────────────────┐  │
│  │ Class Name                       ▾     │  │
│  │ ─────────────────────────────────────  │  │
│  │ Student Last Name                      │  │
│  │ Student Grade                          │  │
│  └────────────────────────────────────────┘  │
│                                               │
│  ┌─────────┐  ┌─────────┐                   │
│  │ Cancel  │  │Generate │                   │
│  └─────────┘  └─────────┘                   │
└──────────────────────────────────────────────┘

Every element in this form was generated automatically from the table metadata. No HTML written. No JavaScript validation coded. No dropdown population logic.

The user selects "Spring 2025", "Math 101", and "All Teachers", then clicks Run. The platform passes:

{
  filters: {
    CoopID: "1",
    TermID: "3",
    ClassID: "5",
    TeacherID: null  // "All Teachers" = no filter
  },
  sort: {
    field: "ClassName",
    direction: "asc"
  }
}

The server receives this structured object and builds the appropriate query. The template merges with only the filtered records.


Advanced Field Configuration

Multi-Column Display Fields

When you want dropdowns to show first name + last name instead of just one field:

{
  "name": "TeacherID",
  "type": "lookup",
  "references": "teachers",
  "displayField": "FirstName,LastName",
  "displayFormat": "{FirstName} {LastName}"
}

Generates dropdown options like:

Sarah Johnson
Michael Martinez  
Emily Chen

Instead of:

Sarah
Michael
Emily

Conditional Options

Some dropdowns should only show relevant options based on other selections:

{
  "name": "ClassID",
  "type": "lookup",
  "references": "classes",
  "displayField": "ClassName",
  "filterBy": "TermID",
  "description": "Filters classes to only those in the selected term"
}

With filterBy, when a user selects "Spring 2025" for TermID, the ClassID dropdown automatically updates to show only classes from Spring 2025, not all classes in the database.

This is cascade filtering, handled automatically by the form generator.

Date Range Inputs

Financial reports often need date ranges:

{
  "name": "StartDate",
  "type": "date",
  "required": true,
  "defaultValue": "firstDayOfMonth",
  "description": "Report start date"
},
{
  "name": "EndDate",
  "type": "date",
  "required": true,
  "defaultValue": "today",
  "description": "Report end date"
}

The form generator recognizes fields named StartDate/EndDate or FromDate/ToDate and renders them as a date range:

Start Date *     End Date *
┌──────────┐     ┌──────────┐
│ 02/01/26 │     │ 02/23/26 │
└──────────┘     └──────────┘

Checkbox Groups for Multiple Selection

Sometimes users need to select multiple values:

{
  "name": "Grades",
  "type": "multiselect",
  "options": ["Pre-K", "K", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
  "required": false,
  "description": "Select one or more grade levels"
}

Renders as checkboxes:

Grades
□ Pre-K    □ K      □ 1     □ 2
□ 3        □ 4      □ 5     □ 6
□ 7        □ 8      □ 9     □ 10
□ 11       □ 12

User selects 3, 4, and 5. The filter becomes WHERE Grade IN ('3', '4', '5').


Designing for Form Generation

When designing your table fields, think about how they'll appear in parameter forms:

1. Foreign Keys Should Always Be Lookups

Bad — stores ID as plain text:

{
  "name": "ClassID",
  "type": "text",
  "required": true
}

Good — enables smart dropdown:

{
  "name": "ClassID",
  "type": "lookup",
  "required": true,
  "references": "classes",
  "displayField": "ClassName"
}

2. Use Descriptive Field Names

Field names become form labels automatically:

Bad — unclear abbreviation:

{
  "name": "TID",
  "type": "lookup",
  "references": "teachers"
}

Form label: "TID" ← Users confused

Good — self-documenting:

{
  "name": "TeacherID",
  "type": "lookup",
  "references": "teachers"
}

Form label: "Teacher" ← Users understand (ID suffix stripped automatically)

3. Provide Options for Select Fields

Select fields need defined options or they render as text inputs:

Bad — no options defined:

{
  "name": "ShirtSize",
  "type": "select"
}

Renders as: <input type="text"> ← Users can type anything

Good — explicit options:

{
  "name": "ShirtSize",
  "type": "select",
  "options": ["YS", "YM", "YL", "S", "M", "L", "XL"]
}

Renders as: <select> with 7 valid options ← Constrained input

4. Make Required Fields Actually Required

If a filter is essential for the report to make sense, mark it required:

{
  "name": "CoopID",
  "type": "lookup",
  "required": true,  // ← User must select co-op
  "references": "coops",
  "displayField": "CoopName"
}

If it's an optional refinement, leave it optional:

{
  "name": "TeacherID",
  "type": "lookup",
  "required": false,  // ← "All Teachers" is valid
  "references": "teachers",
  "displayField": "FirstName,LastName"
}

The Reports Group and Template Cards

In Chapter 4.5, we introduced the Reports table group with an empty tables array:

{
  "id": "reports",
  "name": "Reports",
  "icon": "📊",
  "description": "Document templates and reports",
  "tables": []
}

When users click the Reports group, the platform doesn't show tables — it shows document template cards. Each card displays:

  • Template name
  • Category badge
  • Brief description
  • Tables used (primaryTable + relatedTables)
  • "Run" button

Clicking "Run" opens the automatically-generated parameter form we've been discussing.

This is the complete user journey:

  1. Browse: User sees Reports group (📊), clicks it
  2. Select: User sees 20 template cards in a grid, clicks "Class Roster"
  3. Preview: Modal shows full template metadata (category, description, tables, use case)
  4. Run: User clicks "Run" button
  5. Configure: Auto-generated form appears with filters pre-set to sensible defaults
  6. Refine: User adjusts filters (select specific term, class, teacher)
  7. Execute: User clicks "Run" — dialog closes, template loads into Word, data merges

Steps 4-7 all happen because you defined your table structure correctly in domain-config.json. No additional code.


Validation and Error Handling

The form generator includes built-in validation:

Client-Side Validation

Before submitting the form, the platform validates:

  • Required fields: All fields with required: true must have values
  • Data types: Date fields must be valid dates, number fields must be numeric
  • Range constraints: If field definition includes min/max, values must fall within range
  • References: Lookup fields must select an option from the dropdown (not free text)

If validation fails, the form shows inline error messages:

Academic Term *
┌────────────────────────────────┐
│ Select a term...         ▾     │
└────────────────────────────────┘
⚠️ Academic term is required

Server-Side Validation

After client submission, the server validates:

  • Foreign key integrity: Selected IDs exist in referenced tables
  • Data consistency: Cross-field validation (StartDate < EndDate)
  • Authorization: User has permission to access requested data

If server validation fails, the dialog stays open and displays the server error message.


Testing Your Form Generation

Before publishing your domain, test the generated forms:

Test 1: All Templates Have Parameters

For each template in documentTypes, click "Run" and verify:

  • [ ] Parameter form appears
  • [ ] All expected filter fields present
  • [ ] Required fields marked with asterisks
  • [ ] Dropdown options populate correctly
  • [ ] Default values make sense
  • [ ] Validation works (try submitting empty required fields)

Test 2: Filter Logic Produces Correct Results

Generate the same template with different filter combinations and verify data changes appropriately:

  • No filters: Should return all records
  • Single filter: Only records matching that filter
  • Multiple filters: AND logic (all conditions must match)
  • Sort options: Order changes correctly

Test 3: Edge Cases

  • What if a referenced table is empty? (e.g., no teachers in system yet)
  • What if all filter fields are optional and user leaves them blank?
  • What if lookup table has 100+ options? (pagination, search)

The platform handles all these cases automatically, but test them to ensure your table definitions support the edge cases gracefully.


Migration: Adding Form-Friendly Metadata to Existing Tables

If you have an existing domain without rich field metadata:

// Before — minimal metadata
{
  "name": "ClassID",
  "type": "text"
}

Enhance to enable form generation:

// After — rich metadata
{
  "name": "ClassID",
  "type": "lookup",
  "required": false,
  "references": "classes",
  "displayField": "ClassName",
  "filterable": true,
  "description": "Filter to specific class or leave blank for all classes"
}

This is a non-breaking change as long as you don't change field names or make previously-optional fields required. Increment your domain version from 1.3.0 to 1.4.0 and republish.

Existing users will see enhanced parameter forms the next time they generate reports.


Summary

Automatic form generation is what makes the Data Publisher platform scalable for domain developers. Define your table structure once with rich metadata. The platform handles:

  • ✅ Rendering appropriate input controls based on field types
  • ✅ Populating dropdowns from related tables
  • ✅ Validating user input client-side and server-side
  • ✅ Passing structured parameters to the merge engine
  • ✅ Building filter queries automatically

Your 20 templates get 20 parameter forms with zero additional code.

The key is comprehensive field metadata in domain-config.json:

  • Use type: "lookup" for all foreign keys
  • Specify references and displayField for smart dropdowns
  • Mark essential filters as required: true
  • Set filterable: false for fields that don't make sense as filters
  • Provide options arrays for select fields
  • Use descriptive field names that become clear form labels

With table groups (Chapter 4.5) providing intuitive navigation and automatic form generation (this chapter) providing zero-code parameter collection, your domain is ready for template generation — that's Chapter 5.