# IvyForms Extension API (Lite)

Lite exposes **hooks and registries** so add-ons (ivyforms-pro, agency packs, custom plugins) can extend behavior without lite importing add-on code.

## Rules for extenders

1. **Never** add `class_exists('IvyFormsPro…')`, `IVYFORMS_PRO_*` constants, or Pro-specific UI strings in lite.
2. **Register** features from the add-on via WordPress filters/actions or `window.IvyForms` hooks.
3. **Persist** extension-specific data through form settings hooks (`sanitized_form_data`, `factory/extract_settings`, `persistence/settings_blob`) or field `additional_properties` pipelines, plus the form builder extension registry.
4. Hook names use: `ivyforms/{domain}/{action}` (backend) and the same string on `IvyForms.hooks` (frontend admin/public).

## Bootstrap order

1. Lite `Plugin.php` builds DI → `do_action('ivyforms/boot/extend_container_builder', $builder)`
2. Add-on merges services/repositories in that action (priority 5 typical).
3. `plugins_loaded` → Lite integrations → `do_action('ivyforms/integrations/register', $registry)`
4. Admin/public scripts → `ivyforms:ready` → add-on registers JS hooks and `registerFormBuilderExtension`

## Backend hooks

### Infrastructure

| Hook | Type | When fired | Payload |
|------|------|------------|---------|
| `ivyforms/boot/extend_container_builder` | action | Before container `build()` | `ContainerBuilder $builder` |
| `ivyforms/rest/register_additional_routes` | action | REST bootstrap | `$container`, `$namespace` |
| `ivyforms/integrations/register` | action | Integration registry init | `IntegrationRegistry $registry` |
| `ivyforms/admin/enqueue_scripts` | action | Admin script handle | `$scriptId`, `$page` (menu slug) |
| `ivyforms/shortcode/enqueue_scripts` | action | Public shortcode scripts | `$scriptId` |
| `ivyforms/admin/backend_labels` | filter | Before `wp_localize_script` labels | `array $labels` |
| `ivyforms/shortcode/frontend_labels` | filter | Public labels | `array $labels` |
| `ivyforms/global/settings/get_all` | filter | Settings API response | `$organized`, `$allSettings` |
| `ivyforms/changelog/get_data` | filter | Changelog merge | `$changelogData` |
| `ivyforms/changelog/response_data` | filter | Changelog REST response | `$response`, `$changelogData` |

### Form lifecycle

| Hook | Type | Purpose |
|------|------|---------|
| `ivyforms/form/can_use_form_type_conversational` | filter | Gate `formType=conversational` (default `false`) |
| `ivyforms/form/allowed_form_types` | filter | Extend allowed form type strings |
| `ivyforms/form/sanitized_form_data` | filter | Mutate sanitized save payload (inbound REST) |
| `ivyforms/form/factory/extract_settings` | filter | On load: merge keys from decoded `settings` JSON into `$data` |
| `ivyforms/form/persistence/settings_blob` | filter | Before DB write: merge extension keys into `forms.settings` JSON |
| `ivyforms/form/before_submission` | action | Before entry save |
| `ivyforms/form/after_submission` | action | After entry save (`$entryId`) |

### Field lifecycle

| Hook | Type | Purpose |
|------|------|---------|
| `ivyforms/field/filter_allowed_types` | filter | Add allowed field type strings |
| `ivyforms/sanitizer/field_properties` | filter | Sanitize single field array |
| `ivyforms/field/factory/extract_settings` | filter | Extract field settings from DB JSON |
| `ivyforms/field/value_object/set_properties` | filter | Hydrate field VO |
| `ivyforms/field/entity/set_properties` | filter | Hydrate field entity |
| `ivyforms/field/value_object/init_additional_properties` | filter | Field VO extension init |
| `ivyforms/field/entity/init_additional_properties` | filter | Field entity extension init |
| `ivyforms/field/value_object/add_to_array_additional_properties` | filter | Field VO serialization |
| `ivyforms/field/entity/add_to_array_additional_properties` | filter | Field entity serialization |
| `ivyforms/repository/field/settings` | filter | DB settings read/write |
| `ivyforms/placeholder/filter_field_value` | filter | Placeholder resolution |

### Entry display

| Hook | Type | Purpose |
|------|------|---------|
| `ivyforms/entry/field/resolve_value` | filter | Raw value for entry |
| `ivyforms/entry/field/resolve_value/{type}` | filter | Per-type resolve |
| `ivyforms/entry/field/format_value` | filter | Formatted display value |
| `ivyforms/entry/field/format_value/{type}` | filter | Per-type format |
| `ivyforms/entry/field/before_create` | filter | Before entry field row |
| `ivyforms/entry/field/after_create` | action | After entry field row |

### Import/export & templates

| Hook | Purpose |
|------|---------|
| `ivyforms/form/import_export/*` | Export/import payload and hooks |
| `ivyforms/template/template_mapper` | Register form templates |

## Frontend hooks (`IvyForms.hooks`)

Registered via `api.hooks.addFilter(hook, callback)` / `applyFilters(hook, value, ...args)`.

### Form builder

| Hook | Default | Purpose |
|------|---------|---------|
| `ivyforms/form_builder/canvas_prepend_enabled` | `false` | Show canvas prepend region for `formType` |
| `ivyforms/form_builder/canvas_prepend` | `[]` | Vue components prepended to builder canvas |
| `ivyforms/form_builder/canvas_append_enabled` | `false` | Show canvas append region for `formType` |
| `ivyforms/form_builder/canvas_append` | `[]` | Vue components appended to builder canvas |
| `ivyforms/form_builder/settings_panel_component` | `null` | Options tab component when extension block selected |
| `ivyforms/form_builder/deselect_ignore_selectors` | `[]` | Extra CSS selectors that should not clear builder selection on canvas click |
| `ivyforms/form_builder/prepare_save_payload` | passthrough | Mutate REST save body |
| `ivyforms/form_builder/general_settings_conversational_section` | `null` | Pro conversational settings block (toggle, confirmation, permalink panel) |
| `ivyforms/form_builder/conversational_unsupported_field_types` | `[]` | Field type tokens that block enabling conversational mode (Pro registers e.g. `nps`, `likert`) |
| `ivyforms/builder/filter/collapse_items` | base list | Field palette groups |
| `ivyforms/builder/add/transform` | field | Default field when dropping type |
| `ivyforms/builder/filter/routes` | routes | Extra admin routes |
| `ivyforms/builder/can_resize_field` | `true` | Whether a builder field shows resize handles |

### Fields & render

| Hook | Purpose |
|------|---------|
| `ivyforms/field/filter/component` | Admin field component by type |
| `ivyforms/template/field/filter/component` | Template preview field component |
| `ivyforms/form-render/filter_fields_visible` | Public visibility |
| `ivyforms/form-render/filter_parse_field` | Public field parse pipeline |
| `ivyforms/form/render_layout_component` | `null` | Pro layout component for licensed form types (e.g. conversational) |
| `ivyforms/entry/html_field_types` | Entry admin HTML field types |

### Settings & app

| Hook | Purpose |
|------|---------|
| `ivyforms/integrations/list` | Integration cards metadata |
| `ivyforms/settings/add_integration_subitem` | Settings submenu |
| `ivyforms/settings/add_menu_item` | Settings nav |
| `ivyforms/settings/handle_menu_route` | Route handling |
| `ivyforms/settings/detect_active_route` | Active route detection |
| `ivyforms/app/banner_components` | App-level banners |

### Field options registry

Use `api.fieldOptions.register({ id, fieldTypes, component, tab, order })` instead of hardcoding component names in lite.

## Form builder extension registry

Lite file: `frontend/src/extensions/formBuilderExtensions.ts`

Exposed on admin boot:

```ts
window.IvyForms.registerFormBuilderExtension({
  id: 'myFeature',
  storageKey: 'myFeature', // REST key
  getDefaults: () => ({ ... }),
  normalize: (raw) => ({ ... }),
  isActiveForFormType: (formType) => formType === 'my_form_type',
  builderCanvas: 'prepend', // or 'append'
  settingsPanelContext: 'canvas_prepend',
})
```

Lite store (`useFormBuilder`):

- Keeps `extensionData: Record<storageKey, object>`
- Includes active extensions on save when `isActiveForFormType` matches
- Loads/normalizes from API response per registered extension
- **Selection:** `selectedBuilderEntity` — `{ kind: 'canvas', id }` for extension canvas blocks, `{ kind: 'submit-button' }` for submit button; fields stay on `selectedField`
- **BC:** `welcomePage` / `progressAndButtons` computed aliases and `patchWelcomePage()` / `patchProgressAndButtons()` remain for existing Pro components

Register extensions inside `onLiteReady()` so `registerFormBuilderExtension` exists. Set `builderCanvas: 'prepend' | 'append'` so lite can resolve selection and options panel without hardcoding extension ids. Canvas root elements should use `data-ivyforms-builder-canvas="{extension.id}"` (legacy selectors can be added via `ivyforms/form_builder/deselect_ignore_selectors` until migrated).

## Window API (admin)

| Member | Purpose |
|--------|---------|
| `IvyForms.api` / `IvyForms.hooks` | Hook system |
| `IvyForms.fields.registerAdmin` / `registerPublic` | Field components |
| `IvyForms.fieldOptions` | Field options tab registry |
| `IvyForms.components(name, Comp)` | Named Vue components (legacy; prefer hooks) |
| `IvyForms.formBuilder` | Pinia form builder store |
| `IvyForms.registerFormBuilderExtension` | Builder data extensions |
| `ivyforms:ready` | Lite admin/public ready |

## Form settings persistence (Pro)

Pro extensions that store data in `forms.settings` JSON use three PHP filters:

| Hook | Purpose |
|------|---------|
| `ivyforms/form/sanitized_form_data` | Validate/strip extension payload on save |
| `ivyforms/form/factory/extract_settings` | Lift extension keys from DB `settings` JSON on load |
| `ivyforms/form/persistence/settings_blob` | Merge extension keys into `forms.settings` on save |

See [form-settings-extension-hooks-refactor.md](./form-settings-extension-hooks-refactor.md) for the hook model.

Implementation guides in `ivyforms-pro/docs/` are filled in after feature review — do not treat in-flight Pro code as a canonical reference for AI tooling.
