Create mode — `item` is null, fields generated from schema:

```vue
<template>
  <div>
    <NcButton type="primary" @click="show = true">Add contact</NcButton>
    <CnFormDialog
      v-if="show"
      ref="formDialog"
      :item="null"
      :schema="schema"
      @create="onCreate"
      @close="show = false" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      show: false,
      schema: {
        title: 'Contact',
        properties: {
          name: { type: 'string', title: 'Full name', description: 'First and last name' },
          email: { type: 'string', title: 'Email', format: 'email' },
          phone: { type: 'string', title: 'Phone number' },
          status: {
            type: 'string',
            title: 'Status',
            enum: ['active', 'inactive', 'pending'],
            default: 'active',
          },
          notes: { type: 'string', title: 'Notes', description: 'Optional notes', contentMediaType: 'text/plain' },
        },
        required: ['name', 'email'],
      },
    }
  },
  methods: {
    async onCreate(formData) {
      await new Promise(resolve => setTimeout(resolve, 600))
      this.$refs.formDialog.setResult({ success: true })
    },
  },
}
</script>
```

Edit mode — pre-populate form with `item` data:

```vue
<template>
  <div>
    <NcButton @click="show = true">Edit contact</NcButton>
    <CnFormDialog
      v-if="show"
      ref="formDialog"
      :item="item"
      :schema="schema"
      @edit="onEdit"
      @close="show = false" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      show: false,
      item: { id: 1, name: 'Jane Smith', email: 'jane@example.com', status: 'active', notes: '' },
      schema: {
        title: 'Contact',
        properties: {
          name: { type: 'string', title: 'Full name' },
          email: { type: 'string', title: 'Email', format: 'email' },
          status: { type: 'string', title: 'Status', enum: ['active', 'inactive', 'pending'] },
          notes: { type: 'string', title: 'Notes', contentMediaType: 'text/plain' },
        },
        required: ['name', 'email'],
      },
    }
  },
  methods: {
    async onEdit(formData) {
      await new Promise(resolve => setTimeout(resolve, 600))
      this.$refs.formDialog.setResult({ success: true })
    },
  },
}
</script>
```

## Additional props

### Functional props

| Prop | Default | Description |
|---|---|---|
| `dialogTitle` | `''` | Dialog title. Defaults to `'Create {schema.title}'` or `'Edit {schema.title}'` when empty. |
| `initialData` | `{}` | Seed values for CREATE mode, keyed by field. Merged over the schema defaults when opening a new-item form. Use it to pre-link a child to its parent when adding from a detail page (e.g. `{ lead: '<uuid>' }`). |
| `lockedFields` | `[]` | Field keys rendered read-only (disabled) and immutable — typically the parent reference seeded via `initialData` so the user can't repoint a child away from the record it was created under. |
| `excludeFields` | `[]` | Array of field keys to exclude from the auto-generated form. |
| `includeFields` | `null` | Array of field keys to include (whitelist mode). Null means all fields. |
| `fieldOverrides` | `{}` | Per-field override objects passed to `fieldsFromSchema`. |
| `nameField` | `'title'` | Which field is the "name" of the item (used in result messages). |
| `size` | `'normal'` | NcDialog size — `'small'`, `'normal'`, or `'large'`. |
| `dynamicLoadingLabel` | `'Loading the fields this choice adds.'` | Text shown while the fields a chosen value brings with it are being fetched. See *Fields the data decides* below. |

### Slots

| Slot | Description |
|---|---|
| `before-fields` | Rendered before the first auto-generated field (after the `#form` slot check). Useful for adding introductory text or non-schema inputs. |
| `after-fields` | Rendered after the last auto-generated field. |
| `form` | Replace the entire auto-generated form. Scoped: `{ fields, formData, errors, updateField }`. |
| `field-{key}` | Replace a single auto-generated field. Scoped: `{ field, value, error, updateField }`. |
| `field-{key}-option` | Customize dropdown option rendering for a select/multiselect/tags field. |
| `field-{key}-selected-option` | Customize selected option display for a select/multiselect/tags field. |

### Label customization

All user-visible strings have props so they can be pre-translated by the consumer app.

| Prop | Default (English) | Description |
|---|---|---|
| `successText` | `'{title} saved successfully.'` | Message shown after a successful save. |
| `cancelLabel` | `'Cancel'` | Label for the dismiss button before the action is confirmed. |
| `closeLabel` | `'Close'` | Label for the dismiss button after the result is shown. |
| `confirmLabel` | `''` | Confirm button label. Defaults to `'Create'` or `'Save'` depending on mode. |

## Fields the data decides

A schema property may carry `x-openregister-extends-form`, declaring that picking its value brings further fields with it: a case type's extra questions, a product line's attributes. The dialog fetches those definitions on selection and renders them as ordinary fields, keyed `x-prop:<definition id>`.

`confirm` then carries two arguments. The first is the object's own fields; the second is `{ answers, declarations }`, or `null` for a schema that declares nothing. The split matters: a value row references the parent object, so it cannot be written in the same call, and posting a dynamic key to the parent schema would have OpenRegister drop it silently.

```js static
async onConfirm(formData, dynamic) {
  const saved = await store.saveObject('dossiq/case', formData)
  if (!dynamic) return
  for (const row of valueRecordsFor(dynamic.answers, dynamic.declarations[0].config, saved.id)) {
    await store.saveObject('dossiq/caseProperty', row)
  }
}
```

Full reference: [fields the data decides](../../docs/utilities/dynamic-form-fields.md).

## Fields the chosen record answers

Where `x-openregister-extends-form` ADDS fields, `x-openregister-prefill` fills
ones the schema already declares. A case type knows the status a case of its
kind starts in and who normally handles it, so the person filing one should not
have to retype either.

```json static
{
  "caseType": {
    "type": "string",
    "$ref": "caseType",
    "x-openregister-prefill": {
      "fields": {
        "title": "title",
        "status": "initialStatus",
        "assignee": "defaultAssignee"
      }
    }
  }
}
```

`fields` reads as `{ targetProperty: sourceProperty }`, resolved against the
chosen record. Two rules keep it safe:

- **Only an empty target is written.** A title someone typed before picking a
  case type survives, and so does one they typed after. A field that was
  prefilled is no longer empty, so switching case type again does not overwrite
  the first type's answer either.
- **Create mode only.** In edit mode a blank field is a decision someone
  already made about an existing record, and filling it on open would rewrite
  that decision.

A source the record leaves empty writes nothing, so a case type with no default
assignee prefills the status it does know rather than blanking the assignee.
Add `schema` and `register` to the block when the record does not live in the
picker's own `$ref` target.

## Two-column layout

`columns: 2` pairs the fields up, which roughly halves the scrolling on a form
that asks more than a handful of questions. Pair it with `size="large"`, or the
two columns are merely two narrow ones.

```html static
<CnFormDialog :schema="caseSchema" size="large" :columns="2" />
```

Textareas, JSON editors and code editors still span the full width, and the
layout collapses back to one column below 700px. A manifest `open-form` action
declares the same two keys directly:

```json static
{ "id": "new-case", "type": "open-form", "size": "large", "columns": 2 }
```

### Asking for it from a host component

This dialog is mounted by five components, and each names the two keys the way
that component is configured. A page takes props, a widget reads its content
blob, and the manifest keys follow whichever it is.

| Host | Keys | Declared as |
|---|---|---|
| `CnActionButtons` (manifest `open-form`) | `size`, `columns` | action entry |
| `CnIndexPage` | `formSize`, `formColumns` | `pages[].config` |
| `CnDetailPage` | `formSize`, `formColumns` | `pages[].config` |
| `CnObjectListWidget` | `formSize`, `formColumns` | widget `content` |
| `CnObjectDataWidget` | `formSize`, `formColumns` | props |

`CnObjectListWidget` builds its create form from the whole target schema, so it
also reads `formIncludeFields`, `formExcludeFields` and `formFieldOverrides` off
the same content blob. A list scoped to four columns otherwise opened a create
dialog asking every property the schema declares, which is why consumers reached
for `allowCreate: false` instead of configuring it.

Only `CnActionButtons` forwarded `size` and `columns` before nextcloud-vue
2.46.0. The other four ignored both, so an app could open a two-column create
form from a detail page's header action and a one-column one for the same kind
of record from its index page's Add button, with nothing in either manifest to
explain the difference.

## Conditional field visibility (`condition` / `visibleWhen`)

A field can declare a `condition` (alias `visibleWhen`) descriptor that hides
the field until another field in the same form holds a matching value. The
condition is evaluated on every render against the current `formData`; when a
field transitions visible → hidden, its form-data value is cleared so stale
values are never submitted.

Supported predicates:

| Predicate | Shape | Passes when |
|---|---|---|
| `equals` | `{ field, equals: <scalar> }` | `formData[field] === equals` |
| `notEquals` | `{ field, notEquals: <scalar> }` | `formData[field] !== notEquals` |
| `in` | `{ field, in: [<scalar>, …] }` | `in` array contains `formData[field]` |
| `notIn` | `{ field, notIn: [<scalar>, …] }` | `notIn` array does NOT contain `formData[field]` |
| `truthy` | `{ field, truthy: true }` | `Boolean(formData[field]) === true` |
| `falsy` | `{ field, falsy: true }` | `Boolean(formData[field]) === false` |

Example — show an `arguments` JSON editor only when `jobClass` is a synchronisation action:

```js
[
  {
    key: 'jobClass',
    widget: 'select',
    label: 'Job class',
    enum: ['OCA\\OpenConnector\\Action\\SynchronizationAction', 'OCA\\OpenConnector\\Action\\PingAction'],
  },
  {
    key: 'arguments',
    widget: 'json',
    label: 'Arguments',
    condition: { field: 'jobClass', equals: 'OCA\\OpenConnector\\Action\\SynchronizationAction' },
  },
]
```

Hidden fields are also skipped by the built-in required-fields check and by
`validate()`, so a required-but-hidden field never blocks the confirm button.

Unknown predicates (none of `equals` / `notEquals` / `in` / `notIn` / `truthy` /
`falsy` present) log a warning and keep the field visible — a safer default than
silently hiding a user-facing input.

## Integration single-entity widgets (AD-18)

| Prop | Default | Description |
|---|---|---|
| `referenceContext` (`reference-context`) | `null` | Object context `{ register, schema, objectId }` forwarded to the integration single-entity widget rendered for fields that declare a `referenceType`. Optional. |
