# Custom Code: `window.ffFormAPI`

## Overview

FillFaster forms can run **custom HTML/JavaScript** from the form settings (`htmlCode`). That code runs in the filler's browser, typically to show, hide or require fields based on other answers.

Hiding a field with CSS does **not** change its validation. A field marked `required: true` still blocks submission when hidden.

`window.ffFormAPI` is a small, stable bridge from custom code into the live form. It lets form scripts show and hide fields, toggle required state, read and write values, run validity checks and add validators.

## Availability

| Condition | `window.ffFormAPI` |
|-----------|-------------------|
| Form has custom code (`htmlCode` is not empty) | **Defined**, for the current form |
| Form has no custom code | **Not defined** |
| Form reset (after a submit or a created link) | Re-attached to the new form |

## Custom code lifecycle

1. The form loads and `ffFormAPI` is attached.
2. The `<script>` and `<style>` elements of the custom code are added to the page.
3. When the questions are rendered, the page posts `window.postMessage({ action: 'form_questions_loaded' })`.

New code: put everything inside `ffFormAPI.ready(...)` (see [Conditional logic helpers](#conditional-logic-helpers)). It waits for that message, and runs right away if it already arrived:

```javascript
window.ffFormAPI && window.ffFormAPI.ready(function () {
  // Your logic here
});
```

Older code waits for the message itself. It keeps working:

```javascript
window.addEventListener('message', function (event) {
  if (event.data && event.data.action === 'form_questions_loaded') {
    if (!window.ffFormAPI) {
      console.error('ffFormAPI is not available on this form.');
      return;
    }
    // Your logic here
  }
});
```

## Field keys vs row ids

| Purpose | Identifier | Example |
|---------|------------|---------|
| `ffFormAPI` methods | **Field key** (the field's `name`) | `field48`, `field10 copy` |
| The field's row on the page | **Row id** on `.form-row` | `field-field48`, `field-field10 copy` |

Pattern: row id = `field-` + field key. In new code use `ffFormAPI.getRow(key)` instead of building the id.

## Writing custom code (agents and developers)

### 1. Get the real fields first

```
GET https://api.fillfaster.com/v1/form/{fid}/custom-code-context
```

No token. `fid` is the id in the form link (`fillfaster.com/fill/{fid}`). Returns `{ fid, formName, lang, schemas, htmlCode }`: the form's fields and its current custom code. Forms with an access code or 2+ signers return `403 form_not_public`; ask the customer to contact FillFaster support (support@fillfaster.com) with the form id.

**Never write a field key that is not in this response.** If a field the customer describes is missing or ambiguous, ask which field they mean (list `fieldName` + `name`). Do not guess.

### 2. Which entries are fields

`schemas` is one array per PDF page. Each entry's `name` is the field key and `fieldName` is the label the filler sees. The form turns them into questions like this:

- **Same label, one field.** Entries that share a `fieldName` (the same answer printed in several places) become one question, keyed by the **first** entry's `name`. Use that key.
- **No row:** `type: 'helper'` entries, and anything without a `validationType`.
- **Row, always hidden:** `validationType: 'date_today'` and entries with `hideInput: true` (filled by the sender or prefill).
- **Row, no value:** `validationType: 'html'` / `'external_link'` (static text). `showFields` / `hideFields` / `getRow` work on them; value and validation methods do not.
- **`multi_variable_text`:** one text field per variable, keyed `{name}_{variable}`, labelled with the variable name.
- **Value types:** checkbox `true`/`false`; yes/no the translated word (use `isYes` / `isNo`); select and radio the option text; date a `Date`; file/image an array; table an array of row objects.
- **`required`** is the starting state. For a field that is required only when shown, set it to not required in the form builder and use `showWhen(..., { required: true })`.

### 3. Pick the helper for the use case

| Customer asks | Write |
|---|---|
| Show fields when a checkbox is checked | `api.showWhen('cb', true, [...])` |
| Show fields when a yes/no answer is Yes (or No) | `api.showWhen('q', true, [...])` / `api.showWhen('q', false, [...])` |
| ...and make them required while shown | add `{ required: true }` |
| Show fields for some select / radio options | `api.showWhen('sel', ['A', 'B'], [...])` |
| Show fields when a field has any value (date, text) | `api.showWhen('f', function (v) { return !!v; }, [...])` |
| Show the next group once the previous one is filled (child 1, 2, 3...) | chain `showWhen` from a key field of each group to the next group |
| Fill one field from another (select → code) | `api.onValueChange('sel', function (v) { api.setValue('code', MAP[v] || ''); })` |
| Only one of several checkboxes | `onValueChange` on each; when one turns `true`, `setValue(other, false)` on the rest |
| Lock a prefilled field | `api.setReadOnly('f', true)` |
| Custom validation (age, unique ID, phone) | `api.addValidator('f', fn)`; see [`addValidator`](#addvalidatorfieldkey-validatorfn), and `revalidateFields(..., { markTouched: 'invalid' })` for cross-field rules |
| A "continue" button that opens a section only when fields are valid | `areFieldsValid` + `revalidateFields(..., { markTouched: true })`, then `showFields` |

Everything goes inside one `ready` call:

```html
<script>
window.ffFormAPI && window.ffFormAPI.ready(function () {
  var api = window.ffFormAPI;
  // One line per rule. Comment each rule with the labels it uses.
});
</script>
```

### 4. Rules

- Read and write values only through `ffFormAPI`, never through `input.value`, `dispatchEvent`, widget CSS classes, `MutationObserver`, label text or `localStorage`. The page markup is not part of the contract and has changed before.
- Never compare yes/no answers to `'כן'` / `'Yes'`. Use `isYes` / `isNo` or `showWhen(key, true|false, ...)`.
- Do not hide rows in CSS; `showWhen` / `hideFields` hide them on load. CSS is for layout and styling only.
- No `setTimeout` to wait for the form. `ready` already waits.
- Keep everything inside the `ready` callback so nothing leaks into globals.
- Custom code is not a security boundary. It runs in the filler's browser.
- **Multiple signers:** when a template with several signers has custom code, the server checks only signature fields as required for each signer. Required rules for other fields are enforced by the form only.

### 5. When the form already has custom code

`htmlCode` in the context response is the code on the form now.

1. Read it and list, in plain words, what each part does.
2. Ask the customer: **add** the new rule and keep the existing code as it is, or **rewrite** the code to the helpers above (same behavior, shorter, and it keeps working when the form changes)?
3. If rewriting: keep every rule's behavior, map each old pattern to its helper (table below), and keep anything that has no helper (custom CSS, signature-pad tweaks, iframes) unchanged.
4. Return the full new code, not a diff, with a short list of what changed.

| Old pattern | Helper |
|---|---|
| `window.addEventListener('message', ... 'form_questions_loaded')` | `ffFormAPI.ready(fn)` |
| `showHideListOfFields([...rows], 'show'/'hide')` + `setRequired` + `setValue(key, '')` | `showFields` / `hideFields(keys, { clear: true })` |
| `input.checked`, radio `value === 'כן'`, `getValue(key) === 'Yes'` | `isYes(key)` / `showWhen(key, true, ...)` |
| `MutationObserver` or click listeners on a select's markup | `onValueChange(key, fn)` |
| `input.value = x; input.dispatchEvent(new Event('input'))` | `setValue(key, x)` |
| `document.getElementById('field-' + key)` | `getRow(key)` |
| `input.readOnly = true` | `setReadOnly(key, true)` |
| Finding fields by label text | the `name` from the context response |

### 6. What to hand back

- One block to paste into the form builder: **Settings → Advanced Settings → Custom HTML Code** (Hebrew UI: "הגדרות מתקדמות" → "קוד HTML מותאם אישית"): `<script>` (and `<style>` only if needed).
- Under it, a short list: each rule in the customer's words, with the field labels it uses.
- Tell the customer to set the conditional fields to not required in the builder when you used `{ required: true }`, and to test the form link before sending it.

### Patterns without a helper

**Hide "Receive a copy by email" on the success screen, keep Download** (CSS on a page class; re-check if the success screen changes):

```html
<style>
.get-submitted-pdf-container > *:nth-child(n+4) { display: none; }
</style>
```

**Dynamic checkbox list written into a text field** (options depend on a select or a table): build the checkboxes inside `getRow(textKey)`, hide the text input, and on every change `api.setValue(textKey, checked.join(', '))`.

**Signature pad size or colour:** possible, but it depends on page internals and can break when the form changes. Write it only when the customer asks, and test on mobile.

**Embedding the form in an iframe and listening to its events** is not custom code; it is the embed flow (`window.parent.postMessage` with `status: 'submitted_successfully'`).

## API reference

All methods log a console warning and return `false` when the field key is missing (unless noted).

### `setRequired(fieldKey, isRequired)`

Makes a field required or optional. Its other checks (ID number, phone, email, pattern) stay as they are. The asterisk next to the label follows the change.

```javascript
window.ffFormAPI.setRequired('field48', true);   // make required
window.ffFormAPI.setRequired('field48', false);  // make optional
```

**Returns:** `boolean`: whether the field was found.

### `areFieldsValid(fieldKeys)`

Returns `true` only if every listed field is currently valid. Missing fields are treated as valid.

```javascript
var ok = window.ffFormAPI.areFieldsValid(['field1', 'field2', 'field3', 'field4']);
if (!ok) {
  window.ffFormAPI.revalidateFields(['field1', 'field2', 'field3', 'field4'], { markTouched: true });
}
```

**Returns:** `boolean`

### `revalidateFields(fieldKeys, options?)`

Re-runs the checks on the listed fields.

| `options.markTouched` | Behavior |
|-----------------------|----------|
| omitted / `false` | Revalidate only |
| `true` | Revalidate and show errors on **all** listed fields |
| `'invalid'` | Revalidate all; show errors only on **invalid** fields |

Use `'invalid'` for cross-field rules (e.g. uniqueness) so sibling fields refresh without marking valid ones.

```javascript
// Cross-field uniqueness: refresh siblings, show errors only on duplicates
window.ffFormAPI.revalidateFields(['field2', 'field10', 'field10 copy'], { markTouched: 'invalid' });

// Gate a section: show errors on every invalid field in the list
window.ffFormAPI.revalidateFields(['field1', 'field2', 'field3', 'field4'], { markTouched: true });
```

**Returns:** `void`

### `getValue(fieldKey)`

Reads the field's current value (e.g. `Date` for date fields, string for text).

**Returns:** the value, or `undefined` if not found.

### `setValue(fieldKey, value)`

Sets the value, marks the field as touched, and re-runs its checks. Use when clearing a hidden section (e.g. after removing a dynamic row).

```javascript
window.ffFormAPI.setValue('field12', '');
```

**Returns:** `boolean`: whether the field was found.

### `addValidator(fieldKey, validatorFn)`

Adds a check from custom code.

- `validatorFn(value)` must return `null` (valid) or a **string** error message (invalid). The message is shown under the field.

```javascript
window.ffFormAPI.addValidator('field12', function (value) {
  if (!value) return null; // let required handle empty
  var d = new Date(value);
  if (d > new Date()) return 'לא ניתן לבחור תאריך עתידי';
  // ... age check, etc.
  return null;
});
```

**Note:** Each call **adds** another check. Register once per field.

**Returns:** `boolean`: whether the field was found.

## Conditional logic helpers

These cover most custom code: show, hide and require fields by another field's value. They read the form's values, not the page markup, so they keep working when the form's look changes. Prefer them over DOM listeners, `MutationObserver`, label-text lookups and `setTimeout`.

**Compatibility contract:** custom code on live forms calls these names. Only add methods or options. Never rename one or change what it does.

### `ready(cb)`

Runs `cb` once the questions are rendered, or right away if they already are. Call every other helper inside it.

### `onValueChange(fieldKey, cb)`

Calls `cb(value)` now with the current value (prefill included) and again on every change. Listeners stay active when the form resets after a submit or a created link, and rerun on the new form. **Returns:** `boolean`.

### `showFields(fieldKeys, options?)` / `hideFields(fieldKeys, options?)`

Show or hide the rows (`#field-{key}`) and keep validation in step:

- Hiding removes `required`, so a hidden field never blocks submit. Showing it again restores `required` if it had it.
- `showFields(keys, { required: true })` also makes the fields required (not required in the builder + shown = required).
- `hideFields(keys, { clear: true })` empties the hidden fields. It does not clear while rules are first applied on load, so prefilled values survive.

Showing removes the row's inline `display` style, so fields hidden in the form builder stay hidden. Do not hide rows with CSS in custom code: a CSS-hidden row stays hidden after `showFields`. Let `showWhen` / `hideFields` hide them on load.

Keys of headings or static text have a row and no value; they are shown and hidden like any field.

### `showWhen(triggerKey, condition, fieldKeys, options?)`

Shows `fieldKeys` while `triggerKey`'s value matches `condition`, hides them otherwise. Built on `onValueChange`, so it runs on load and after every change.

| `condition` | Matches when |
|---|---|
| `true` | yes/no is Yes, or checkbox is checked (`isYes`) |
| `false` | yes/no is No, or checkbox is unchecked (`isNo`). Unanswered yes/no matches neither |
| a value, e.g. `'Married'` | value equals it (select, radio, text) |
| an array of values | value is one of them |
| `function (value) { ... }` | the function returns truthy |

`options`: `{ required: true }` as in `showFields`; `clear` defaults to `true` (hidden answers are not submitted). Pass `{ clear: false }` to keep them. **Returns:** `boolean` (trigger found).

### `isYes(fieldKey)` / `isNo(fieldKey)`

Yes/no answers are stored as the **translated** word (`'Yes'` in English, `'כן'` in Hebrew), and checkboxes as `true`/`false`. Comparing to a literal word breaks when the form language changes. Use these instead; they accept both forms in any language. A yes/no question nobody answered is neither yes nor no.

### `setReadOnly(fieldKey, readOnly)`

Locks or unlocks a field. The value is still submitted, and a locked field is not validated. Stays locked after the form resets. **Returns:** `boolean`.

### `getRow(fieldKey)`

Returns the field's row element (`#field-{key}`) or `null`. Use it instead of building the id yourself when you need the page element, e.g. to insert a button after a row.

### Example

```javascript
window.ffFormAPI && window.ffFormAPI.ready(function () {
  var api = window.ffFormAPI;

  // Married checkbox → spouse details, required while shown
  api.showWhen('field39', true, ['field24', 'field25'], { required: true });

  // Yes/no "has a third party?" → third-party details
  api.showWhen('field41', true, ['field3 copy', 'field5 copy', 'field12']);

  // Select → extra fields for two of its options
  api.showWhen('field15', ['Purchase', 'Sale'], ['field74', 'field82']);

  // Prefilled vehicle number stays as sent
  api.setReadOnly('vehicle_number', true);
});
```

## Typical patterns

### Gate a section until employee fields are valid

```javascript
continueBtn.addEventListener('click', function () {
  var keys = ['field1', 'field2', 'field3', 'field4'];
  if (!window.ffFormAPI.areFieldsValid(keys)) {
    window.ffFormAPI.revalidateFields(keys, { markTouched: true });
    return;
  }
  window.ffFormAPI.showFields(['field48', 'field10', 'field11'], { required: true });
});
```

## Security and scope

- The API exists only on forms whose owner added custom code.
- It works on the current form only.
- It does not bypass server-side checks; anything security-critical must be checked on the server.

