# Veldtypen

De ondersteunde veldtypen en hun instellingen.

## Veldtypen

| Type | Beschrijving |
|---|---|
| `text` | Tekst op één regel |
| `textarea` | Platte tekst op meerdere regels |
| `html` | Opgemaakte tekst (bubble-editor en veldbalk, zie Editorknoppen) |
| `slug` | URL-veilig slugveld, berekend uit de ingestelde bronvelden |
| `boolean` | Schakelaar waar/onwaar |
| `date` | Datumkiezer |
| `email` | E-mailadres |
| `url` | URL |
| `password` | Versleuteld wachtwoordveld |
| `image` | Upload van één afbeelding |
| `file` | Upload van één bestand |
| `upload` | Algemene upload |
| `gallery` | Upload van meerdere afbeeldingen |
| `select` | Keuzelijst, met vaste opties of uit een resource |
| `number` | Getalinvoer, met optioneel `min` en `max` |
| `color` | Kleurkiezer |
| `location-picker` | Interactieve wereldkaart die de breedtegraad in het veld bewaart en de lengtegraad in het bijbehorende veld uit `longitude_field` |

Namen van veldtypen moeten precies kloppen.

## Veldinstellingen

Gangbare opties per veld:

| Sleutel | Type | Beschrijving |
|---|---|---|
| `name` | string | Label in de admin |
| `required` | boolean | Validatie; zet dit op alle titel- en naamvelden |
| `admin_col` | boolean | Tonen in de overzichtstabel van de admin (standaard: true) |
| `admin_filter` | boolean of object | Voegt een filter toe aan het overzicht van de admin voor een veld `select` of `boolean`. Gebruik `{ "default": <stored-value> }` om het aan te zetten met een standaardkeuze, en optioneel `"feature": "permission-name"` om het alleen te tonen aan gebruikers met dat recht. Los van `admin_col`. |
| `multi` | boolean | Meerdere waarden toestaan |
| `i18n` | boolean | Per taal vertalen |
| `accept` | string | Beperking van bestandstypen voor image- en file-velden |

**HTML-velden** gebruiken uitdrukkelijk een van twee instellingen:

Eenvoudig (korte tekstblokken, zoals een intro):
```json
"buttons": "bold,italic",
"admin_col": false
```

Uitgebreid (volledige content, zoals een body):
```json
"buttons": "h2,h3,h4,bold,italic,orderedlist,unorderedlist,quote,anchor",
"media": true,
"media_sizes": "sm-90,md-70,lg-60,xl-50,xxl-40",
"admin_col": false
```

Definieer nooit een `html`-veld zonder een van deze te kiezen. `media_sizes` is verplicht bij `media: true`.

**Editorknoppen.** `buttons` noemt de opmaakknoppen van een `html`-veld, in volgorde. Zonder `buttons` krijgt een veld `bold,italic,removeFormat`. Ingebouwd:

| Knop | Doet |
|---|---|
| `bold`, `italic` | Opmaak in de tekst (Ctrl/Cmd+B, Ctrl/Cmd+I) |
| `h2`, `h3`, `h4`, `quote`, `pre` | Blokopmaak; nog een keer maakt er weer een alinea van |
| `orderedlist`, `unorderedlist` | Lijsten |
| `anchor` | Link: toevoegen, bewerken (vooraf ingevuld) of verwijderen; Ctrl/Cmd+K |
| `removeFormat` | Opmaak wissen |
| `strikethrough`, `subscript`, `superscript`, `underline` | Optioneel, in geen enkele standaardset |

Dezelfde knoppen staan op twee plekken: de bubble-werkbalk bij geselecteerde tekst, en de veldbalk. In admin-formulieren hoort de veldbalk bij het veld; bij inline bewerken zweeft hij boven het actieve veld en bevat hij ook Media (bij `media: true`) en Save (Ctrl/Cmd+S). Alt+F10 zet de toetsenbordfocus op de veldbalk. Plakken houdt alleen platte tekst over, tenzij het veld `"paste_html": true` zet; dat houdt basisopmaak vast en haalt de markup van Word en Google Docs weg.

**Eigen editorknoppen.** Een project definieert eigen knoppen als JSON in `ext/tpl/bubble-editor-buttons/index.tpl` (de template mag shortcodes gebruiken, zoals `[#text ...#]` voor labels), en noemt ze dan net als de ingebouwde in de `buttons` van een veld:

```json
{
    "highlight": { "kind": "wrap", "tag": "mark", "label": "[#text Highlight#]", "icon": "M" },
    "h5": { "kind": "block", "tag": "h5", "label": "Heading 5", "icon": "H5" },
    "poi": {
        "kind": "event", "event": "nb:insert-point", "insert": true, "bubble": true,
        "active": "[data-poi-link]", "label": "[#text Point of interest#]", "icon": "<svg ...></svg>"
    }
}
```

| Soort | Doet |
|---|---|
| `command` | Een opmaakcommando (`"command": "bold"`) |
| `block` | Blokopmaak (`"tag": "h5"`) |
| `list` | Lijstcommando |
| `wrap` | Zet de selectie in een element (`"tag"`, optioneel `"class"`); nog een keer haalt het weg |
| `insert` | Voegt vaste HTML in bij de cursor (`"html"`) |
| `event` | Stuurt een DOM-event op het veld (`"event"`, bubbling, `detail: { editor, range }`) voor eigen code van het project, bijvoorbeeld om een kiezer te openen |
| `link` | Vraagt om een URL, zoals `anchor` |

Opties: `"insert": true` zet een knop naast Media in de veldbalk in plaats van bij de opmaakknoppen; voeg `"bubble": true` toe als hij ook op geselecteerde tekst werkt; `"active"` is een selector die de knop actief toont als de cursor in een match staat. Links zijn `a[href]`: een project mag `<a>` zonder `href` gebruiken voor eigen elementen in de tekst. Code kan ook knoppen registreren met `nb.bubble_editor.register(name, { label, icon, run(ctx), is_active(ctx) })`.

De bewerktools van een project (de modal en het script achter een `event`-knop) staan in `ext/tpl/edit-extras/index.tpl`, die wordt getoond voor gebruikers die inline mogen bewerken, op pagina's van de site en van de admin.

**Slugvelden:**

Slugs zijn aparte velden. Zet geen `"slug": true` op een ander veld. Definieer een apart veld met `"type": "slug"` en een `source` die wijst naar het veld of de velden waar hij van wordt afgeleid.

Het admin-formulier berekent de slug in de browser uit de bronvelden, voordat het formulier wordt verstuurd. De API verzint bij het opslaan geen ontbrekende slugvelden; hij bewaart de waarde van het slugveld die hij krijgt. Maakt of importeert code records buiten het admin-formulier om, dan moet die code ook de waarde van het slugveld zetten.

Zet `"preserve_existing": true` op het slugveld om ontbrekende slugs te maken terwijl opgeslagen URL's gelijk blijven als het formulier opent of de brontitel verandert. Redacteuren kunnen een opgeslagen slug nog steeds bewust aanpassen.

```json
"url_slug": {
  "name": "URL slug",
  "type": "slug",
  "source": "title",
  "admin_col": false
}
```

Meerdere bronnen scheid je met komma's; de waarden worden met een spatie samengevoegd voordat er een slug van wordt gemaakt:

```json
"url_slug": {
  "name": "URL slug",
  "type": "slug",
  "source": "title,date",
  "admin_col": false
}
```

Met `"source": "title,date"`, titel `"Jazz Night"` en datum `"2026-04-09"` wordt de slug `jazz-night-2026-04-09`. Zet `url_slug` altijd in de lijst `index` van de resource, zodat hij in `route.inc` kan worden opgezocht.

Is het bronveld i18n, dan wordt het slugveld ook als i18n behandeld. Zo geven `title.nl`, `title.en` en `title.fr` overeenkomende waarden onder `title_slug.nl`, `title_slug.en` en `title_slug.fr`. Vertaal slugs niet met de hand; laat het slugveld ze afleiden uit de bronwaarden per taal.

**Selectievelden:**

Vaste opties:
```json
"type": "select",
"options": {
  "new": "New",
  "active": "Active",
  "closed": "Closed"
}
```

Opties uit een andere resource:
```json
"type": "select",
"resource": "categories"
```

Filters in het overzicht van de admin zet je per veld aan voor `select`- en `boolean`-velden:

```json
"status": {
  "type": "select",
  "options": { "active": "Active", "closed": "Closed" },
  "admin_filter": { "default": "active" }
},
"published": {
  "type": "boolean",
  "admin_col": false,
  "admin_filter": true
}
```

Filters vergelijken de ruwe opgeslagen waarden, terwijl ze de ingestelde opties of labels van de gekoppelde resource tonen. Selecties met meerdere waarden passen als het record de gekozen waarde bevat. Meerdere filters worden met EN gecombineerd en blijven werken met zoeken, sorteren en pagineren. Keuzes blijven in de URL staan als `filter[field]=value`. Een ontbrekende parameter gebruikt de ingestelde standaard; een lege parameter kiest uitdrukkelijk **All**. Ongeldige waarden in de URL worden **All**.

**Locatievelden:**

Gebruik `location-picker` als redacteuren coördinaten op een kaart moeten kiezen in plaats van ze te typen. Het kiezerveld bewaart de breedtegraad in zijn eigen veld en werkt een apart numeriek veld voor de lengtegraad bij, genoemd in `longitude_field`. Verberg het bijbehorende veld met `wrapper_class: "hidden"`, zodat de redacteur één bedieningselement ziet terwijl de resource eenvoudige losse coördinaten bewaart:

```json
"location_lat": {
  "name": "Location",
  "type": "location-picker",
  "longitude_field": "location_lng",
  "required": true,
  "admin_col": false
},
"location_lng": {
  "name": "Longitude",
  "type": "number",
  "required": true,
  "min": -180,
  "max": 180,
  "wrapper_class": "hidden",
  "admin_col": false
}
```

## Hoe formuliervelden worden opgebouwd

Deze sectie beschrijft de hele weg van een velddefinitie in een resource tot een getoond formulierveld. Lees dit voordat je een nieuw veldtype bouwt.

### Overzicht

```
.meta field definition
  ↓
render_field() - PHP (core/modules/forms/lib/render-field.php)
  ↓
_f.* template variables
  ↓
[#field-{type}#] template (core/modules/forms/tpl/field-{type}/index.tpl)
  ↓
Alpine.js form_data.{field}   ← x-model binding
  ↓
API POST/PUT on submit
```

### `render_field()` en de context `_f.*`

`render_field()` is de ingang. Het:

1. Neemt een array met de velddefinitie, de sleutel van het veld, en optionele parameters voor waarde, opslag en bron
2. Verspreidt de hele velddefinitie over `_f.*` via `set_variable_dot('_f', $def)`; elk attribuut uit de `.meta` is automatisch beschikbaar in de template
3. Zet daar berekende variabelen bovenop: `_f.key`, `_f.title`, `_f.value`, `_f.model`, `_f.required`, `_f.ai`, `_f.bg`
4. Geeft door aan `[#field-{type}#]`, de template voor dat veldtype

`_f.model` is de Alpine.js-expressie voor `x-model`, berekend als `{store}.{field}`. De standaardopslag is `form_data`. Bij i18n-velden wordt het `form_data.{field}[{lang}]`.

Standaardvariabelen `_f.*` die in elke veldtemplate beschikbaar zijn:

| Variabele | Inhoud |
|---|---|
| `_f.key` | Veldnaam / HTML-attribuut name |
| `_f.title` | Label (uit `name` in .meta) |
| `_f.value` | Vooraf ingevulde waarde |
| `_f.model` | Alpine-expressie voor x-model, bijvoorbeeld `form_data.email` |
| `_f.required` | Of het veld verplicht is |
| `_f.bg` | Class voor de achtergrondkleur van het zwevende label |
| `_f.*` (elke) | Alle andere sleutels van de velddefinitie: `_f.accept`, `_f.options`, `_f.resource`, enzovoort |

### `form_data`: de Alpine.js-store

`form_data` is het reactieve Alpine.js-object met alle huidige veldwaarden. Het wordt op het element `<form>` gedeclareerd. Elk veld schrijft zijn waarde hierin via `x-model="[#_f.model#]"`.

Bij het versturen wordt `form_data` in de API-payload gezet en met POST naar `/api/v1/{resource}` gestuurd. Velden met een rijke editor (HTML, gallery, image) schrijven hun waarden via een apart mechanisme (`nb.edit.get_field_values()`), dat automatisch wordt samengevoegd; veldtemplates hoeven dat niet zelf te regelen.

### De eenvoudigste veldtemplate

`field-default/index.tpl` is de basis voor tekstachtige velden:

```html
<div class="[#_f.wrapper_class#]">
    <input type="[#_f.type#]" value="[#_f.value#]" name="[#_f.key#]"
        x-init="[#_f.model#]=`[#_f.value#]`"
        x-model="[#_f.model#]"
        [#if _f.required=(not-empty) echo=required#]
        class="input input-bordered w-full" />
    <label class="pointer-events-none absolute left-3 -top-2.5 px-1
            font-bold text-sm leading-tight [#get _f.bg default=bg-neutral-50#]">
        [#_f.title#][#if _f.required=(not-empty) echo=" *"#]
    </label>
</div>
```

### Een nieuw veldtype toevoegen

1. Maak de template:
   ```
   core/modules/forms/tpl/field-{type}/index.tpl   ← core types
   ext/tpl/field-{type}/index.tpl                   ← project-specific types
   ```

2. In de template:
   - Koppel aan Alpine via `x-model="[#_f.model#]"`
   - Zet de beginwaarde met `x-init="[#_f.model#]='[#_f.value#]'"`
   - Gebruik `[#_f.key#]` als HTML-attribuut `name`
   - Lees opties van het type via `_f.*` (bijvoorbeeld `[#_f.accept#]` voor beperkingen van bestandstypen)

3. Gebruik de typenaam in `.meta`; hij werkt zodra de template bestaat.

4. Beheert het veld ingewikkelde status (zoals een bestandskiezer of een knop voor de mediabrowser), nest dan een eigen `x-data` binnen de `x-data` van het formulier. De buitenste `form_data` blijft bereikbaar vanuit geneste scopes.

Eigen velden gebruiken de afspraak `field-{type}` voor templates en de context `_f.*` (`_f.model`, `_f.key`, `_f.title`, `_f.bg`, enzovoort). Veldtemplates uit een oudere generatie, met `_fmodel`, `_fid`, `_ftitle`, `_fbg`, `_bf_render_field()` of Tailwind Elements-attributen `data-te-*`, worden in deze vorm opnieuw gebouwd in plaats van overgenomen.

Geneste velden en herhaalvelden moeten de geneste modelpaden van Alpine zelf regelen. Een veld `group` dat `locations` bewaart, koppelt de onderliggende velden bijvoorbeeld aan paden zoals `form_data.locations[ix].location`, of geeft een expliciet model mee bij hergebruik van `render_field()`. Ga er niet van uit dat de renderer van core geneste arraypaden uit een platte veldsleutel kan afleiden.
