Ga naar inhoud

Formulieren — tips & valkuilen

Praktische aandachtspunten bij het bouwen en registreren van Open Formulieren in deze stack. Het formulier "Laravel NAW-formulier" is het levende voorbeeld: definitie in config/forms/laravel-naw/form.py, opbouw via config/forms/laravel-naw/deploy.sh.

De meeste van deze punten gelden omdat wij formulieren via een script (de manage.py shell) opbouwen i.p.v. via de admin-UI. De UI vult dan namelijk een aantal dingen automatisch in die wij zelf moeten zetten.


0. Een formulier live krijgen (deployen)

Een formulier "ontwikkelen" = de definitie schrijven in een *-form.py-script. Live krijgen = dat script uitvoeren tegen de juiste omgeving. De definitie zet zelf al active=True en maintenance_mode=False, dus na een succesvolle run staat het formulier publiek op https://<of-host>/<form-slug>.

Stappen voor een nieuw formulier:

  1. Map + definitie schrijven. Maak config/forms/<slug>/ aan — daar staat alles van één formulier bij elkaar. Kopieer config/forms/skeleton-form.py naar config/forms/<slug>/form.py en vul de TODO-blokken (velden, logica, categorie, registratie). Voeg config/forms/<slug>/objecttype.json (objecttype- definitie) en config/forms/<slug>/deploy.sh (kopie van een bestaand formulier) toe. Houd de valkuilen hieronder aan.

  2. Randvoorwaarden klaarzetten (eenmalig per omgeving, in deze volgorde):

  3. Objecttype waarin de inzending wordt geregistreerd → scripts/bootstrap-objecttype.sh <env> (scant config/objecttypes/*.json én config/forms/*/objecttype.json; zet de OBJECTTYPE_*_UUID in .env).
  4. Services + permissies (objects-groep, DRC/Catalogi) → ./commonground-run.sh <env> setup.
  5. Documenttypen (PDF "Aanvraag", upload "Bijlage", evt. "Brief") in de catalogus → scripts/bootstrap-informatieobjecttype.sh <env>.

  6. Het script uitvoeren in de draaiende openforms-container (idempotent — je kunt het zo vaak draaien als je wilt, het doet update_or_create):

docker compose --env-file .env exec -T \
  -e OBJECTTYPE_UUID="<uuid van je objecttype>" \
  -e OBJECTS_GROUP="objects" \
  -e CATALOGUS_DOMEIN="LAR" -e CATALOGUS_RSIN="123456782" \
  -e IOT_SUBMISSION_REPORT="Aanvraag" -e IOT_ATTACHMENT="Bijlage" \
  openforms python src/manage.py shell < config/forms/<slug>/form.py

Maak hiervoor bij voorkeur een eigen wrapper naar het model van config/forms/laravel-naw/deploy.sh: die leest .env, controleert de randvoorwaarden en draait bovenstaande regel. Zo is de deploy reproduceerbaar en per omgeving (test/acc/prod) hetzelfde.

  1. Controleren. Open https://<of-host>/<form-slug> en doe een testinzending (zie §6); controleer dat het object in de Objects-API en de PDF/bijlagen in de DRC landen.

Bestaand formulier wijzigen: pas het *-form.py-script aan en draai stap 3 opnieuw. Omdat alles via update_or_create (en het opnieuw genereren van stappen, variabelen en logica) loopt, is het script de bron van waarheid — wijzig dus het script, niet de admin-UI, anders raakt de volgende deploy je handmatige wijziging kwijt.

OTAP: draai dezelfde wrapper met testaccprod. Per omgeving horen een eigen .env, objecttype-UUID en catalogus erbij; het *-form.py-script zelf is identiek.

Tijdelijk offline halen kan zónder her-deploy via de admin (formulier op onderhoudsmodus of niet actief), maar bij de volgende scriptrun worden active/maintenance_mode weer op de scriptwaarden gezet.


1. Upload/file-component moet volledig zijn

Een handmatig aangemaakt file-component met een leeg "file": {} laat het hele formulier in de browser crashen:

TypeError: Cannot read properties of undefined (reading 'length')
    at validationSchema.js

De SDK-renderer leest file.type als array (o.a. file.type.map(...)). Ontbreekt die, dan klapt het validatieschema om en wordt het formulier niet getoond. Zet minimaal:

{"type": "file", "key": "bijlage", ...,
 "file": {"type": ["*"], "allowedTypesLabels": ["alle bestandstypen"]},
 "filePattern": "*", "useConfigFiletypes": False, "url": ""}

Cosmetische console-meldingen zoals cookiebar.js … Loading the font 'data:font…' violates … font-src of blocked-uri: eval zijn geen blokkers — die breken de render niet. Zoek bij "formulier wordt niet getoond" altijd naar de échte rode TypeError.


1b. Keuze-componenten hebben openForms.dataSrc nodig

Een handmatig aangemaakt radio-, select- of selectboxes-component met alleen de Form.io-opties (values / data.values) toont het formulier prima aan de invuller, maar laat de bouwer-editmodal in de admin crashen zodra je de component-details opent:

edit.js Uncaught TypeError: Cannot read properties of undefined (reading 'dataSrc')

De admin-bouwer leest component.openForms.dataSrc om te bepalen waar de opties vandaan komen (vaste lijst / variabele / referentielijst). Ontbreekt de openForms-namespace, dan klapt de editmodal om en zie je niets. Zet bij elk keuze-component minimaal:

{"type": "selectboxes", "key": "faciliteiten", "label": "...",
 "values": [...],
 "openForms": {"dataSrc": "manual", "translations": {}}}

dataSrc: "manual" = een vaste, in het component opgenomen optielijst. Een handige normalisatie bij script-matig bouwen (zie ../config/forms/evenementenvergunning/form.py):

CHOICE_TYPES = {"radio", "select", "selectboxes"}
def normaliseer(components):
    for c in components:
        if c.get("type") in CHOICE_TYPES:
            of = c.setdefault("openForms", {})
            of.setdefault("dataSrc", "manual")
            of.setdefault("translations", {})
    return components

Voorwaardelijke OPTIES binnen één selectboxes: dataSrc: variable + itemsExpression

De opties van een keuzecomponent laten afhangen van een eerder antwoord is een OF-native feature: zet op het component openForms.dataSrc = "variable" en openForms.itemsExpression = een JsonLogic-expressie die de optielijst teruggeeft. Bijvoorbeeld (uitgebreide lijst bij locatie_type = buiten, anders basis):

"openForms": {"dataSrc": "variable", "itemsExpression": {"if": [
    {"==": [{"var": "locatie_type"}, "buiten"]},
    [["ehbo", "EHBO-post"], ["tent", "Tent"]],   # buiten
    [["ehbo", "EHBO-post"]],                       # anders
]}}

⚠️ Itemsformaat (dé valkuil): elk item moet een scalar zijn (waarde = label) of een 2-elementen-lijst [waarde, label]. Een dict {"value":.., "label":..} laat json-logic crashen met "Unrecognized operation value"HTTP 500 op de stap-API. (OF leest items via normalise_option in formio/dynamic_config/dynamic_options.py: een lijst → [0]=waarde, [1]=label, anders → scalar.)

Doe dit niet via een logica-regel met een property-actie die de eigenschap values overschrijft: dan krijgt de invuller "ongeldige invoer" bij het indienen en is de regel in de admin onzichtbaar. Een prima alternatief blijft twee aparte selectboxes (basis altijd zichtbaar, "extra" via de component-conditional) als je liever helemaal geen variabele-opties gebruikt.


2. Registratie Objects-API: gebruik v2 (Variabelekoppelingen)

De plugin kent twee varianten:

Variant version Hoe het object wordt opgebouwd
Variabelekoppelingen (aanbevolen) 2 variables_mapping: koppel variabelen aan paden
Verouderd (sjabloon) 1 content_json Django-template

v1 werkt nog, maar is gemarkeerd als verouderd. Gebruik v2 met variables_mapping, bijvoorbeeld:

"variables_mapping": [
    {"variable_key": "public_reference", "target_path": ["of_referentie"]},
    {"variable_key": "pdf_url",          "target_path": ["pdf"]},
    {"variable_key": "attachment_urls",  "target_path": ["bijlagen"]},
    {"variable_key": "voornaam",         "target_path": ["naw", "voornaam"]},
    # ...
]

Beschikbare registratie-variabelen (naast de component-variabelen): public_reference, pdf_url, csv_url, attachment_urls, submission_id, plus de payment_*-variabelen.


3. Documenttypen: catalogus + omschrijving (niet de URL-velden)

De keuzelijsten onder "Documenttypen" in het registratiescherm worden gevuld door de catalogus + omschrijving-variant. De directe URL-velden (informatieobjecttype_*) zijn legacy (verdwijnen in OF 3.0) en laten die keuzelijsten leeg — ook al werken ze functioneel wel. Gebruik dus:

"catalogue": {"domain": "LAR", "rsin": "123456782"},
"iot_submission_report": "Aanvraag",   # documenttype inzendings-PDF
"iot_attachment": "Bijlage",           # documenttype geüploade bijlagen

De omschrijving moet exact overeenkomen met een gepubliceerd informatieobjecttype in die catalogus (zie scripts/bootstrap-informatieobjecttype.sh). De upload naar de Documenten-API gebeurt op basis van iot_attachment — onafhankelijk van of het file-component in de mapping staat.

Afwijkend documenttype per upload-component (override)

iot_attachment is het standaard documenttype voor álle uploads. Wil je voor een specifiek upload-component een ánder documenttype, zet dat dan op het file-component zelf via registration.documentType:

{"type": "file", "key": "brief", "label": "Brief", "multiple": True,
 "storage": "url", "url": "", "useConfigFiletypes": False,
 "file": {"type": ["*"], "allowedTypesLabels": ["alle bestandstypen"]},
 "filePattern": "*",
 "registration": {"documentType": {
     "description": "Brief",                                  # omschrijving van het IOT
     "catalogue": {"domain": "LAR", "rsin": "123456782"},
 }}}

Uploads uit dit component krijgen dan documenttype "Brief" in de Documenten-API; componenten zónder override vallen terug op iot_attachment. Ook de override- omschrijving moet een gepubliceerd IOT in die catalogus zijn.

Documenttype ≠ plek in het object. Het documenttype wordt bij de upload bepaald; waar de URL's in het object terechtkomen bepaalt de mapping. Wil je alle uploads (uit meerdere componenten) in één node verzamelen, koppel dan de registratie-variabele attachment_urls aan dat pad (bv. ["bijlagen"]). Wil je ze juist per component scheiden, koppel dan elk file-component apart (de component-variabele levert dan alleen de eigen upload-URL's). De documenttypen blijven in beide gevallen per upload correct.


4. Statische en samengestelde waarden → gebruikersvariabelen

v2 koppelt alleen variabelen; er is geen sjabloon. Voor waarden die niet één-op-één een formulierveld zijn:

  • Statische waarde (bv. het verplichte type-veld): een gebruikersvariabele met een vaste initial_value.
    FormVariable.objects.create(
        form=form, key="objecttype_type", source=FormVariableSources.user_defined,
        data_type=FormVariableDataTypes.string, initial_value="verzoek-laravel-naw-formulier")
    
  • Samengestelde waarde (bv. volledige naam): een gebruikersvariabele die door een logica-regel wordt gevuld met een JsonLogic-expressie:
    {"cat": [{"var": "voornaam"},
             {"if": [{"var": "tussenvoegsel"}, {"cat": [" ", {"var": "tussenvoegsel"}]}, ""]},
             " ", {"var": "achternaam"}]}
    

Koppel die variabelen daarna gewoon mee in variables_mapping.


5. Logica-regels via script — koppel ze aan de stap!

Dé valkuil bij script-matig aanmaken: de (nieuwe) logica-evaluatie haalt regels op via de M2M FormStep.logic_rules. Zonder die koppeling wordt een regel nooit geëvalueerd (de waarde blijft dan leeg, zonder foutmelding). De admin-UI vult dit automatisch via regel-analyse; bij directe model-aanmaak moet je het zelf doen:

rule = FormLogic.objects.create(form=form, ...)
rule.form_steps.set(list(FormStep.objects.filter(form=form)))

Andere velden van een logica-regel:

  • is_advanced=True — gebruik dit bij niet-triviale triggers/waarden. De visuele (eenvoudige) editor kan een samengestelde cat/if-waarde niet weergeven en geeft anders een renderfout in de admin.
  • description — vrij in te vullen label van de regel (bv. "Stel volledige naam samen"). Dit is wat je als naam in de admin ziet.
  • json_logic_trigger — de leesbare tekst eronder ("Als …") wordt automatisch uit de expressie afgeleid; die is niet los in te typen. Een betekenisvolle trigger ({"!!": [{"var": "achternaam"}]}, "zodra achternaam is ingevuld") leest dus prettiger dan een lege truc als {"==": [1, 1]}.
  • trigger_from_step — vanaf welke stap de regel mag triggeren.

"Deze regel wordt op alle stappen uitgevoerd" — geen fout

Die melding wordt afgeleid uit form_steps: OF toont "alle stappen" zodra de regel aan álle stappen van het formulier hangt. Bij een formulier met één stap is "alle stappen" gelijk aan die ene stap — daar is niets aan te doen en niets mis mee. De specifieke stapnaam verschijnt pas bij meerdere stappen waarbij de regel aan een deel daarvan hangt. form_steps is read-only/automatisch en moét gevuld blijven (zie hierboven).


6. Testinzending via de API (zonder browser)

Handig om end-to-end te testen. Belangrijke punten:

  • Gebruik Django's DRF APIClient(enforce_csrf_checks=False) binnen de container (manage.py shell); dan hoef je niet met CSRF-cookies te stoeien.
  • De volgorde: POST /api/v2/submissions → bijlage uploaden via POST /api/v2/formio/fileuploadPUT …/steps/{uuid} met de stapdata → POST …/steps/{uuid}/_check-logicPOST …/submissions/{uuid}/_complete → status pollen op de statusUrl.
  • De _check-logic-stap is nodig om logica-gezette gebruikersvariabelen (zoals de samengestelde naam) te laten evalueren en persisteren — zoals de echte SDK ook doet. Sla je die over, dan blijft zo'n variabele leeg.
  • Het file-component in de stapdata is een formio-bestandsobject dat verwijst naar de tijdelijke upload, met een niet-lege data.baseUrl (bv. {OF}/api/v2).

Snelle checklist bij "het werkt niet"

  • Formulier toont niet → zoek de rode TypeError in de console (vaak een onvolledig component, niet de CSP-meldingen).
  • Veld komt leeg in het object → staat de variabele in variables_mapping? En bij een logica-variabele: is de regel aan de stap gekoppeld (form_steps)?
  • Keuzelijsten documenttypen leeg → gebruik je catalogue + iot_* i.p.v. de URL-velden?
  • Document upload mist → bestaat het informatieobjecttype (omschrijving) en is het gepubliceerd?