# Introduction

## Introduction

### ![Delfs' Engineering Logo](/files/-LKI59E6I5NuoWTrIwD0)

### Klai Studio

> **A loosely coupled high performance front end web publishing framework and application that allows FileMaker database developers to build anything from form widgets to full web apps.**

Klai Studio makes it easy to create high performance web applications and forms without any deep knowledge of JavaScript or web technologies.

* Mobile first responsive design
* Publish and process forms, data and pages
* Build connected widgets ready to be placed into existing websites
* Publish data and tables
* Use 3rd party JS modules easily

## Contributing

#### Documentation Contributions

If you see something that you can contribute, we would LOVE a pull request with your edits! To make this easy you can click the *"Edit this page"* link at the top of the web docs.

The docs are all written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). If you've used GitHub, it's pretty likely you've encountered it before. You can become a pro in a few minutes by reading their [GFM Documentation page](https://help.github.com/articles/github-flavored-markdown/).

#### Filemaker Code Snippets

If you have Filemaker script code suggestions and code contributions we would love to see them. You can and the files to a pull request or open an issue here.

#### BetterForms Schema trick, and tips

For contributing schema tricks and techniques please create a pull request or open an issue and we will add them to the Tips and tricks document here.

**Author** Delfs' Engineering TORONTO CANADA [www.fmbetterforms.com](http://www.fmbetterforms.com)

***

***


# Features

**FMBetterForms** is a high-performance single-page web application (SPA) platform that allows FileMaker developers to build modern responsive web apps without learning all new technology.

**User Interface**

* **Fully Responsive UI**: Compatible with desktop, tablet, and mobile devices for an optimal user experience.
  * Build installable progressive web apps (PWA's)
* **Custom Themes**: Create and apply custom themes to enhance the visual appeal of your apps.

**Authentication and Security**

* **Multi-Mode Authentication System**:
  * Authenticate via URL hash.
  * Authenticate via username (email) and password.
  * Enable web sign-up and account creation.
* **Developer Hooks**: For password reset, forgot password, and email verification.
* **Role-Based Access Control**: Manage user permissions based on roles.
* **House / Custom Domains** Bring your own custom domain or use a house one.

**Pages and Workflow**

* **Page Design and Workflow Engine**:
  * Multiple page types including plain, multi-step wizard, and Master-Detail layouts.
  * In-browser client-side JavaScript validation for common needs, including custom validation.
  * Optional server-side validation on page/tab change via developer hooks (in FileMaker).
* **Actions Processing Engine**:
  * Allows script-like workflow execution.
  * Automate nearly all aspects of the web application.
  * Actions can be initiated from both the client browser and the FileMaker Server application.

**Data Handling and Integration**

* **Reactive JavaScript Calculation Engine**:
  * Perform on-the-fly reactive calculations for nearly any element or parameter.
  * Write full JavaScript functions that can be called from any action.
* **Data API Gateway**: Integrate with the Data API.
* **XML Gateway**: Integrate with the XML gateway.
* **Multiple Payment Gateways**: Monetize your app with various payment options.

**User Interaction**

* **Summary Modals and Alert Actions**: Facilitate interaction and communication with users.
* **Place Layouts within Modal Card Windows**: Enhance user experience with organized content display.

**Development and Deployment**

* **Hooks Scripts**: Run locally within the existing developer's app, keeping all business logic together.
* **No Installation and Deployment**:
  * Cloud-hosted PaaS application.
  * Single FileMaker Helper file installed on the target FMS box.
  * Seamless updates and rollbacks of your app's base code.
* **Multistage Development Environments**:
  * Develop in an add development environment.
  * Deploy to testing and staging environments.
  * Each environment can have its own or shared FileMaker servers and files.

**Additional Features**

* **Internationalization**: Use the `BF.i18n('key')` function for multi-language support.
* **Analytics and Reporting**: Track app visitation and user interactions for insights.


# Welcome to Klai Studio!

You're signed up! Check your inbox to verify your account.

**Next Steps:**

* Click the button below to go to your dashboard and start exploring.

  [Go to your Dashboard](https://app.fmbetterforms.com/)

**Join Our Community:**

In the meantime, we recommend you join these communities to help you get the most out of Klai Studio:

* **Slack**: Connect with other users and our support team in real-time for assistance and collaboration.

  [Join our Slack Channel](https://app.fmbetterforms.com/#/slack)
* **Forum**: Post your questions, find answers, and participate in discussions with other users.

  [Visit the Support Forum](https://forum.fmbetterforms.com/)

If you encounter any issues, click the "Report a bug" button at the bottom of the screen.


# Overview

Klai Studio was purpose built to allow FileMaker developers access to a full powerful web technology stack.

Klai Studio was purpose-built to allow FileMaker developers access to a full, powerful web technology stack. All data is stored on your FileMaker Server and is not cached by BetterForms. Data remains secure in transit with encryption.

For authentication options and workflows, see the Authentication section in the Reference docs: [Authentication](/reference/authentication).

<figure><img src="/files/GWgBC6hFeFzvo0GykpDI" alt=""><figcaption><p>High-Level Architecture</p></figcaption></figure>

## System Status and Performance Monitor

The health and performance of the BetterForms Cloud can be checked by visiting the official status page:

{% embed url="<https://status.fmbetterforms.com>" %}
Official BetterForms Status Page
{% endembed %}

To receive live updates, you can subscribe via the monitoring page.


# Your First App

After successfully logging in, let's get familiar with the workspace. Below is a screenshot of the main Integrated Development Environment (IDE):

1. **Logo Button**: Located at the top-left corner, this button brings you back to the homepage from any section of the app.
2. **Apps**: This tab allows you to access and manage the different applications you have built or are working on. Here, you can open, edit, or create new apps.
3. **Servers**: In the Servers tab, you can add, delete, and manage server connections and information. This is where you configure the servers your applications will interact with.
4. **File Assets**: This section provides access to all your uploaded files and assets. You can upload new files, organize them, and link them to your apps as needed.
5. **Account**: The Account tab contains your personal and account settings. Here, you can update your profile information, change your password, and manage your subscription. The screenshot below shows an example of the Account tab: \*
   * **Edit Account**: Click this button to update your account details such as your name and email.
   * **Change Password**: Use this option to update your password.
   * **Company Information**: Here, you can manage details about your company.
   * **Users**: In this section, you can add or remove users and change their roles within the account.
   * **Subscription & Billing**: View your subscription details, update your plan, and manage billing information.
6. **Help**: If you need assistance, a Help tab offers various support resources, including documentation, examples & demos, and contact options for technical support.
7. **Report a Bug Button**: Located at the bottom-center, this button allows you to report any issues or bugs you encounter while using the app. Your feedback helps us improve the system.

To start building your first app, click on the [<mark style="color:red;">`"Build your first app"`</mark>](/getting-started/ide-quick-tour/first-app) button located in the Quick Access section.


# 1. Setting Up Your Foundation

This initial part of the guide is essential for laying the groundwork to connect your FileMaker solutions with BetterForms. By completing these steps, you'll establish the core communication channel between your FileMaker Server and the BetterForms Integrated Development Environment (IDE).

**Key Goals for this Section:**

* Ensure your FileMaker Server is correctly configured to allow Data API access.
* Integrate the BetterForms Helper file into your target FileMaker solution.
* Register and validate your FileMaker Server within the BetterForms IDE.

Successfully completing this foundation work is the first step towards building dynamic web applications powered by your FileMaker data.

The articles below will guide you through each setup process:


# 1.1 Configure FileMaker Server

This initial part of the guide is essential for laying the groundwork to connect your FileMaker solutions with BetterForms.

### Requirements

* FileMaker Server v16+ (v17+ preferred for Data API support)
* CWP (XML or Data API) enabled on the server
* A server with adequate CPU and a fast connection (data-center recommended)
* Base Elements Plugin (only used for the integration script in the Helper file)

### Configuration Steps

1. **Download the Helper File:** [Get the latest version here](https://www.dropbox.com/sh/o8c1k649qpret5r/AAAYa7hKiOZEgBlSL4vCs6kma?dl=0).
2. **Change Admin Credentials:** Open the helper file and change the default `admin / admin` credentials to your own secure password.
3. **Set BetterForms User Password:** Change the password for the **`BetterForms`** user account. This is the password the BetterForms servers will use to connect to this file. **Note this password down**, as you will need it when adding the server to the IDE and when setting up your own application files.
4. **Upload to Server:** Upload the configured helper file to your FileMaker Server.
5. **Add Credentials to Your App:** In your own FileMaker application file, create a `BetterForms` user account with the **exact same password** you set in step 3. This account needs a privilege set that allows `Data Entry Only` and has the `fmxml` (for CWP) and/or `fmrest` (for Data API) extended privileges enabled.
6. **Enable CWP Gateway:** Ensure either the XML Custom Web Publishing gateway or the Data API is enabled on your FileMaker Server. For FMS 17+, enabling the XML gateway must be done via the [command line](http://docs.360works.com/index.php/Enable_XML_FileMaker_17).

{% hint style="warning" %}
You must have a valid TLS certificate installed on your FileMaker Server to use the Data API.
{% endhint %}


# 1.2 Install BetterForms Helper File

The BetterForms Helper file contains pre-built custom functions and scripts to accelerate development. This guide shows you how to install these components into your own FileMaker application file.

### What is the "Business File"?

The "business file" is your primary FileMaker application file that you are connecting to BetterForms. The custom functions and scripts only need to be installed in this single, primary file.

### Integration Steps

**1. Install Custom Functions**

1. Open the **Helper File** on your server.
2. Navigate to the **Integration** tab and click the button under **Step 1**. This copies the necessary custom functions to your clipboard.
3. Open your business file, go to `File > Manage > Custom Functions...`, and paste the functions.

**2. Install Scripts**

1. In the helper file's **Integration** tab, go to **Step 2**.
2. Follow the on-screen instructions to generate the scaffold scripts. This will copy a script folder to your clipboard.
3. Open the **Script Workspace** in your business file and paste the scripts.

**3. (Optional) Reconnect the Helper File for Local Debugging**

To run and debug hooks locally from the helper file, you need to connect it to your business file.

1. In the **Helper File**, open the Script Workspace and find the script named `Hooks - Developer - Transmitter- CONFIGURE HERE`.
2. Add your business file as an external data source.
3. Edit the `Perform Script` step to call the `Betterforms - App - Receiver and Dispatch` script in your business file.

This allows the helper file to act as a proxy, which is useful for development and testing.

{% hint style="info" %}
Each FileMaker file that BetterForms interacts with must have the same user credentials. Your BetterForms user account should have \[Data Entry Only] permissions with XML and Data API extended privileges enabled.
{% endhint %}


# 1.3 Add Your Server to BetterForms (IDE)

The final setup step is to register your FileMaker Server with the BetterForms IDE and test the connection.

### Registration Steps

1. Log into the [BetterForms IDE](https://app.fmbetterforms.com/#/servers) and navigate to the **Servers** tab.
2. Create a new server connection, providing a friendly name and the IP address or Fully Qualified Domain Name (FQDN) of your FileMaker server.
3. Click the **Test Connection** button to verify that BetterForms can communicate with your server's Data API or XML gateway. You should see a list of the files hosted on your server, including the helper file.

{% hint style="warning" %}
If your FileMaker Server is configured to **Filter databases...**, you will not see the file list immediately. Instead, you will see a `{"messages":[{"code":"9","message":"Insufficient privileges"}]}` error. You must enter the `BetterForms` user credentials you configured previously to see the list of files.
{% endhint %}

### Troubleshooting

* For FileMaker Server 17+, the XML gateway must be enabled from the command line. See [this guide](http://docs.360works.com/index.php/Enable_XML_FileMaker_17) for instructions.
* If you have a firewall protecting your server, you may need to whitelist the BetterForms IP addresses. See the [Firewalls & Security](/reference/security/security) page for details.


# 2. Building Your First Application

With your foundation in place, this section walks you through the exciting process of creating your first BetterForms application. You'll learn how to use the IDE to build pages, add interactive elements, and set up basic data flows and actions.

**Key Goals for this Section:**

* Create a new application (Site) within the BetterForms IDE.
* Build your first web page using the Page Builder.
* Understand and manage different deployment environments.
* Add and configure various page elements.
* Implement basic validation and actions.
* Get introduced to list views, data flow, and hooks.

This hands-on experience will give you a practical understanding of the BetterForms development workflow.

The articles below will guide you through building your initial application:


# 2.1 Create an App (Site) in the IDE

After [setting up your foundation](/getting-started/ide-quick-tour/setup/configure-fm-server) (including configuring your FileMaker Server and adding it to BetterForms), you're ready to create your first web application, which BetterForms refers to as an "App" or "Site".

## What is an "App" or "Site" in BetterForms?

An App/Site in BetterForms is the front-end code and configuration for an entire web application. Apps can have multiple environments (e.g., development, staging, production), where each environment is technically a separate instance of the app with its own settings and data connections.

To learn more about the comprehensive settings available for an App/Site, you can refer to the [Site Settings Reference](/reference/site-settings) documentation later.

## Steps to Create Your New App

1. **Navigate to the Apps Menu:** In the BetterForms IDE (usually at `https://app.fmbetterforms.com/#/sites`), locate and click the **"Create / New App"** button (or a similar button like **"Build your first app"** if you're a new user).

   <figure><img src="/files/NSKEZbwgYqALM8bJaFdw" alt=""><figcaption></figcaption></figure>
2. **Choose a Starting Point (Optional: Using a Template):**

   * You may be presented with an option to start from scratch or use a pre-built template.
   * If templates are available, you can browse them, view details (often in a popup window), and select one that suits your needs by clicking **"Use This Template"**.

   <figure><img src="/files/sry4NoNxGje1cQnOxvRS" alt="Template Selection Page"><figcaption></figcaption></figure>

   \* {% hint style="success" %} \*\*Template Documentation:\*\* Some templates may have additional specific documentation. \[Check for template-specific guides here]\(../../reference/guides-and-integrations/README.md). {% endhint %}3. \*\*Input App Details:\*\* Whether starting from scratch or a template, you'll need to provide some basic details for your new app:

   <figure><img src="/files/XWpN1x8KX01X4tgz6vlU" alt="Input App Details Form"><figcaption></figcaption></figure>

   \* \*\*App Name:\*\* This is the internal name displayed in the BetterForms editor and on your dashboard. \* \*\*Public Name:\*\* This is the name that can be displayed on your live site (e.g., in the browser title bar). Keep it concise; you can usually change it later. \* \*\*Domain Name:\*\* Enter the desired subdomain for your app (e.g., \`myawesomeapp\`.bfm.app). The system may check for availability. You can configure a custom domain later. \* \*\*Select Your Server:\*\* From the dropdown list, choose the FileMaker Server connection you configured in \[Phase 1.3]\(../setup/add-server-to-bf.md). If you haven't added your server yet, you might be prompted or need to go back to that step. \*(The UI for this might be integrated with the "Input App Details" form or a subsequent step).\*
3. **Configure Core App Settings (often on a "General" tab or similar after initial creation):**
   * **Set Server Credentials:** Click the **"Set Credentials"** button (or a similar section, possibly per environment) to securely enter the username and password for the FileMaker account BetterForms will use to connect to your database. This user should have the necessary privileges as outlined in the server setup.
   * **Common Hook Set Name:** Enter a name for the common hook FileMaker script set for this app. For your first app, using "app" (or a similar default like "bf\_hooks") is typical. This refers to a set of FileMaker scripts that BetterForms will call. You were introduced to hooks in [Phase 1.2](/getting-started/ide-quick-tour/setup/install-bf-helper-file).
   * **Enable Dev Mode (Recommended for Development):** While testing and developing, it's helpful to have **"Dev Mode"** enabled (this might be an environment-level setting). This often provides more detailed error messages and disables certain caching.
4. **Save and Prepare:**
   * Save your app settings. You should see your new app preparing its development environment.
   * If you selected a server during the initial details input, the connection might be tested here. If server selection is a post-creation step, ensure you assign your configured server to the appropriate environment.
5. **Set Helper File Credentials (Environment Specific):**
   * Once the app environment is available (e.g., "development"), navigate to its specific settings. This is often done by clicking a menu (e.g., three dots) associated with the app's development environment and selecting "Credentials" or "Helper File Credentials".
   * Enter the name of your FileMaker helper file (e.g., `YourSolution_BF_Helper.fmp12`) and the FileMaker account credentials that BetterForms will use to interact with the scripts in this helper file (as set up in [Phase 1.2](/getting-started/ide-quick-tour/setup/install-bf-helper-file)).
6. **Test Integration (Optional but Recommended):**
   * You can often test the basic API integration by navigating to `https://YOURDOMAIN/api` (replace `YOURDOMAIN` with your app's assigned domain). If you see a JSON object (often a success message or status), your basic connection is working.

You have now created your first app structure! The next step is to [create your first page within this app](/getting-started/ide-quick-tour/first-app/create-page).


# 2.2 Create Your First Page (Intro to Page Builder)

Now that you have [created your App (Site) structure](/getting-started/ide-quick-tour/first-app/create-app), the next exciting step is to build your first interactive page within it.

## What is a BetterForms Page?

Think of a BetterForms page like a FileMaker layout. It will contain any combination of fields, buttons, data tables, text, labels, etc., that you place onto it. The primary difference is that a BetterForms **page** is not tied to a specific Table Occurrence from your FileMaker solution. Instead, it will have access to whatever data you pass into it individually through its **Data Model**.

Pages are managed within the "Pages" tab of your App in the BetterForms IDE. From there, you can create new pages, edit existing ones, and manage their settings.

{% hint style="warning" %}
This guide assumes that you already have a working knowledge of basic JSON formatting and terminology. If you have never worked with JSON before, we recommend you [review this introduction to JSON](/getting-started/support-and-resources/learning-json) first.
{% endhint %}

## Key Concepts for Your First Page

Before diving into the creation steps, let's touch on two fundamental concepts:

### 1. The Data Model

All data that can be seen and interacted with on the page lives in the **Data Model**. The Data Model is simply a JSON object. It can be pre-populated with data from your FileMaker Server when the page loads, and then users can modify it through input fields or other page elements.

When you run a [**Scoped Hook**](/getting-started/ide-quick-tour/3.-understanding-core-betterforms-concepts/3.1-introduction-to-hooks-and-where-to-find-them-in-the-ide#scoped-hooks) (a FileMaker script tied to this page), the current Data Model is passed with the request, allowing you to process and save the data back to your FileMaker database.

### 2. The Page Schema

The **Page Schema** is where you define all the elements that will appear on your page. It's also structured in JSON format.

The most important part of the schema for now is the `fields` array. This array holds a list of all the [Page Elements](/reference/components-overview) (like input fields, buttons, text areas, etc.) that make up your page.

If you're familiar with HTML, you know that pages are built from the top down by default. The same concept applies here. Using Bootstrap CSS classes (which BetterForms supports), you can control the layout, such as how wide an element should be, and the elements will stack on the page accordingly.

## Step-by-Step: Creating Your First Page

Let's walk through creating a simple page.

1. **Navigate to Create a New Page:**
   * In the BetterForms IDE, within your newly created App, go to the **"Pages"** section.
   * Click the **"New Page"** button (or a similar "+" icon).
2. **Initial Page Settings:**
   * **Page Name:** Give your page a descriptive name, e.g., "My First Page".
   * **Description:** (Optional) Add a brief description.
   * **Scoped Hook Set Name:** You'll need to set a [Scoped Hook Set Name](/getting-started/ide-quick-tour/3.-understanding-core-betterforms-concepts/3.1-introduction-to-hooks-and-where-to-find-them-in-the-ide#how-to-name-hooks) for the page. For this first page, something simple like "page1" or "myFirstPageHooks" will work. This name corresponds to a group of FileMaker scripts that will handle data operations for this specific page.
   * **Authentication:** For this initial page, let's simplify things. Turn the **Authentication toggle to OFF** (it might show as "No Auth" or similar). This means users won't need to log in to see this specific page for now.
3. **Adding a Simple Element to See Data (Schema Editor):**

   * After basic settings, you'll typically be taken to the page editing interface, which includes a **Schema Editor** tab. This is where you define what appears on your page.
   * Your new page might start with some default elements. We're going to add a special element to display the contents of our Data Model.
   * In the Schema Editor, look for a "Snippets" search bar or section. Search for "HTML".
   * You should find a snippet like **"HTML - Data Model Inspector"**. Click the copy button for this snippet.
   * Now, you need to paste this copied JSON snippet into the `fields` array within your page's schema. If there are already elements in the `fields` array, make sure to add a comma (`,`) after the last existing element before pasting the new one.

   Your schema's `fields` array, with the new HTML inspector added, might look something like this (some existing default elements might be present):

   ```json
   {
       "schema": {
           "fields": [
               // ... any existing default elements would be here ...
               // If there were elements above, ensure there's a comma after the last one.
               {
                   "type": "html",
                   "label": "Data Model Inspector",
                   "html": "<pre>{{model}}</pre>",
                   "styleClasses": "col-md-12" // Or col-md-6 depending on template
               }
           ]
       },
       "showPanelHeader": true, // This might be a default setting
       "title": "My First Page" // This often reflects the page name
   }
   ```

   *(Note: The exact structure of default elements can vary. The key is adding the `Data Model Inspector` object correctly into the `fields` array.)*

   {% hint style="info" %} The schema editor might seem overwhelming at first, but it's very well organized.

   * To help you understand the structure, try clicking the small triangles or arrows often found near the line numbers. This allows you to collapse and expand JSON objects or arrays, making it easier to see the hierarchy.
   * Alternatively, while you're learning, you might find it useful to copy your page schema JSON and paste it into an external JSON editor like [jsoneditoronline.org](https://jsoneditoronline.org/) to view and edit it in a different visual format. {% endhint %}
4. **Populating Initial Data (Data Model Tab):**

   * Now, switch to the **"Data Model"** tab for your page. This is where you can define some initial data that your page will have when it loads.
   * Enter a simple JSON object. The "HTML - Data Model Inspector" element we added to the schema will display this data on the page.
   * For example, add:

   ```json
   {
       "nameFirst": "John",
       "nameLast": "Doe",
       "message": "Hello, BetterForms!"
   }
   ```
5. **Save Your Page:**
   * **Crucial step:** Click the **"Save"** button to save all your changes to the page schema and data model.

## Viewing Your New Page

You have two main ways to see your page:

1. **Preview within the IDE:** Most page editors have a **"Preview"** button. This will render your page directly within the BetterForms environment.
2. **Preview in Your Site:** To see it as your users would, you often want to preview it within your actual site. To do this, replace the `app.fmbetterforms.com` part of the preview URL (or the IDE's URL) with your own app's domain that you configured (e.g., `myawesomeapp.bfm.app`).

{% hint style="success" %}
Remember to **save** your page *before* previewing it in a new browser tab or window to ensure you see the latest changes!
{% endhint %}

Go ahead and preview this page in your site. You should see the "Data Model Inspector" element displaying the JSON data you entered:

```
{
  "nameFirst": "John",
  "nameLast": "Doe",
  "message": "Hello, BetterForms!"
}
```

(Or however the `<pre>{{model}}</pre>` renders it).

## Next Steps

Congratulations! You've successfully created a basic page, defined its structure with the schema, added some initial data, and viewed it.

The next step is to learn about [Adding and Configuring Page Elements](/getting-started/ide-quick-tour/first-app/adding-elements) to build more complex and interactive user interfaces.


# 2.3 Understanding & Managing Environments (IDE)

After building your app, you will be taken to the environment management page. Here, you can manage different environments for your app, such as Development, Staging, and Production. This allows you to build, test and deploy your app in a controlled manner.

**Development Environment**

* This is where you will develop and test new features and changes before moving them to staging or production.

**Staging Environment**

* This environment is used for final testing before deployment to production. It mimics the production environment to ensure everything works as expected.
* Click the <mark style="color:red;">`"+ New"`</mark> button under the Staging section to create a staging environment.

**Production Environment**

* This is the live environment where your end users interact with your app. It should only contain thoroughly tested and stable versions of your app.
* Click the <mark style="color:red;">`"+ New"`</mark> button under the Production section to create a production environment.

You can switch between environments and manage their settings as needed to ensure a smooth development and deployment workflow.


# 2.4 Adding Elements to Your Page

You've successfully [created your first page](/getting-started/ide-quick-tour/first-app/create-page) and even added a "Data Model Inspector" to see some raw data. Now, let's explore how to add more structured and interactive elements to your page using the Page Schema editor.

The "Page Builder" or Schema Editor in BetterForms is your primary tool for this. It allows you to define, configure, and organize all the components that make up your user interface.

## Understanding the Page Schema for Elements

As you learned, the **Page Schema** is a JSON structure that defines everything on your page. The `fields` array within the schema is where you'll list the individual elements.

```json
{
    "schema": {
        "fields": [
            // Elements like inputs, buttons, HTML blocks, etc., go here
        ]
    },
    // ... other page settings ...
}
```

Each object you add to the `fields` array represents a distinct element on your page.

## Using Snippets to Add Elements

The easiest way to add common pre-configured elements is by using the **Snippet Library** in the Schema Editor.

1. **Locate Snippets:** In the Schema Editor interface, you should find a section or search bar for "Snippets."
2. **Find an Element:** Search for the type of element you want to add (e.g., "button," "input," "HTML").
3. **Copy and Paste:** Select the desired snippet and copy it. Then, paste this JSON snippet into the `fields` array in your Page Schema. Remember to add a comma (`,`) after the preceding element if you're adding to an existing list.

This is how you added the "HTML - Data Model Inspector" in the previous step. You can use the same process to add input fields, buttons, and more.

## Organizing Elements with Groups

For more complex pages, you can organize your `fields` into logical `groups`. This helps in structuring your schema and can sometimes influence layout.

```json
{
    "schema": {
        "groups": [
            {
                "title": "User Information", // An optional title for the group
                "_note": "Section for user details", // A note for schema organization
                "fields": [
                    { "type": "input", "model": "nameFirst", "label": "First Name" },
                    { "type": "input", "model": "nameLast", "label": "Last Name" }
                ]
            },
            {
                "title": "Actions",
                "fields": [
                    { "type": "button", "label": "Save", "action": "submitData" }
                ]
            }
        ]
    },
    // ...
}
```

* `_note`: This property is purely for your organizational purposes within the schema and doesn't render on the page.

## Common Element Properties

Each element you add will have various properties to configure its behavior and appearance:

* `type`: Specifies the kind of element (e.g., "input", "button", "html"). This is fundamental.
* `label`: Text displayed as the label for an element (e.g., for an input field).
* `model`: For input fields, this links the element to a specific key in your [Page Data Model](/getting-started/ide-quick-tour/first-app/create-page#1-the-data-model).
* `styleClasses`: Allows you to apply CSS classes (e.g., Bootstrap classes like `col-md-6` for layout, or custom classes) to style the element.
* `visible`: Can be set to `false` to hide an element statically.
* `visible_calc`: For dynamic visibility, you can use a calculation based on your data model, e.g., `"visible_calc": "model.isUserAdmin"`.

You'll find many more properties specific to each element type in the [BetterForms Elements reference documentation](/reference/components-overview).

## Basic Input Field Validation

For input fields, you can add validation rules:

* `validator`: Specify a validator type (e.g., `"string"`, `"number"`, `"email"`).
* `validator_calc`: For dynamic validation conditions.
* `errorMsg`: A custom message to display if validation fails.

Example:

```json
{
    "type": "input",
    "model": "email",
    "label": "Email Address",
    "validator": "email",
    "errorMsg": "Please enter a valid email address."
}
```

Validation can also be triggered by actions, like a button click.

## Tools for Managing the JSON Schema

The Schema Editor provides tools to help you work with the JSON structure, especially for complex pages:

* **Folding/Unfolding:** Look for controls (often in a toolbar or near line numbers) to collapse or expand different levels of your JSON schema. This is very helpful for navigating large schemas.
* **Formatting:** There's usually an option to automatically format your JSON, keeping it neat and readable.

## Editing HTML Content Within Elements

If you add an "html" type element (or other elements that embed HTML), the Schema Editor often provides a dedicated HTML editor:

* **Accessing the Editor:** Typically, clicking on the line number of an HTML field in the schema will open up a more user-friendly HTML editing interface.
* **Features:** This editor might offer features like syntax highlighting, and sometimes even direct data model editing or an AI assistant for HTML generation.
* **Saving:** Remember to save changes within the HTML editor, and then save the overall page schema.

## Next Steps

You now have a basic understanding of how to add and configure elements on your BetterForms page using the Page Schema editor. The key is to:

1. Use snippets to add elements quickly.
2. Understand the basic properties like `type`, `model`, and `label`.
3. Refer to the detailed documentation for each specific element type.

Explore the [BetterForms Elements reference](/reference/components-overview) to see the wide variety of available elements and their specific configuration options. As you build out your first application, you'll become more familiar with these tools and properties.


# 2.5 Understanding Validation

Validation is a crucial part of ensuring data integrity in your BetterForms application. This guide will help you understand how to implement and manage validation in your pages.

## Client-Side Validation

Client-side validation is controlled by the `validator` key in all page elements. This validation happens immediately in the browser, providing instant feedback to users.

### Triggering Validation

Validation does not run automatically. You must trigger client validation with the `validate` action. This action can be added to buttons or other elements to validate the form when needed. Automatic validation can be controlled with the validations settings from the `Page Settings` tab withing the page editor.

### Validator Types

The `validator` key can be set to the following types:

* **number**: Checks that the value is numeric and within the fields min & max range, if defined.
* **integer**: Checks that the value is a valid JavaScript Number and an integer.
* **double**: Checks that the value is a valid number.
* **string**: Checks that the value is a string and its length is within the fields min & max range, if defined.
* **array**: Checks that the value is an array and its length is within the fields min & max range, if defined.
* **date**: Checks that the value is a valid JavaScript Date and between the fields min and max dates, if defined.
* **regexp**: Checks that the value matches the regex defined in the fields schema.pattern.
* **email**: Checks that the value is a plausible looking email address.
* **url**: Checks that the value is a plausible looking http url.
* **creditCard**: Checks that the value is a valid credit card number.
* **alpha**: Checks that the value is a letter.
* **alphaNumeric**: Checks that the value is a letter or a number.

## Custom Validators

You can define your own JavaScript custom validation calculation that will fire the same as the regular validators. By selecting the `validator` type to `calc` and adding a calculation and error message, you can have excellent control over validation.

### Example: Custom Validator

Here's an example of how to define a custom validator:

```json
{
  "inputType": "text",
  "label": "Some Password",
  "model": "password1",
  "placeholder": "",
  "styleClasses": "w-80",
  "type": "input",
  "required": true,
  "validator": "string"
},
{
  "inputType": "text",
  "label": "Another Password",
  "model": "password2",
  "placeholder": "",
  "styleClasses": "w-80",
  "type": "input",
  "required": true,
  "validator": "calc",
  "validator_calc": "model.password1 == model.password2",
  "errorMsg": "The passwords do not match"
}
```

## Server-Side Validation

While client-side validation is useful for providing immediate feedback, sensitive data should also be validated in your FileMaker scripts before committing to the database. This ensures that data integrity is maintained even if client-side validation is bypassed.

## Validation Settings

The **Page Settings** tab allows you to manage validation settings for your page:

* **Validate After Loaded**: Triggers validation routines as soon as the page is loaded. This should be enabled if any fields on the page have validators.
* **Validate After Changed**: Triggers validation routines as soon as a field is changed. This should also be enabled if fields have validators to ensure data integrity.

## Next Steps

You now have a basic understanding of how to implement and manage validation in your BetterForms application. As you build out your first application, you'll become more familiar with these components and how to use them effectively.

Explore the [BetterForms Elements reference](/reference/components-overview) to see the wide variety of available elements and their specific configuration options.


# 2.6 Adding Actions to Your Page

Now that you understand the basics of the Page Data Model and Hooks, let's explore how to add actions to your page in BetterForms. This guide will walk you through the process of setting up and running actions.

## Introduction to Actions

Actions are instructions that tell the BetterForms framework to perform some operation. They can be used to create interactions with the user. Actions can be executed sequentially, and some actions block the execution of the next action until they are complete.

## Types of Actions

### Regular Actions

* ***showModal*** - Renders a modal dialog
* ***hideModal*** - Hides a modal that is non-blocking
* ***showAlert*** - Renders a toaster style alert
* ***path*** - redirects the user to a new page
* ***runUtilityHook*** - runs the `onUtility` hook passing it params
* ***downloadFile*** - downloads a file (link) to the user
* ***runOnCompleteHook*** - runs the `onComplete` hook
* ***clipboard*** - runs `clipboard` action allowing interaction with the clipboard
* ***cookie*** - Allows setting of browser side cookies
* ***wait*** - Waits for specified time or event
* ***emit*** - Vue event bus message emit
* ***scrollTo*** - Scrolls to an element
* ***namedAction*** - Runs a named action. Also requires the `name` key.
* ***function*** - Runs JavaScript

### Authentication Actions

* ***authLogin*** - Performs an authentication login
* ***authLogout*** - Performs logout
* ***authReset*** - Performs a password reset action
* ***authForgot** -* Performs a forgotten password reset hook
* ***authVerify*** - Performs a verification of the verify token
* ***authResend*** - Re/sends the email verification token
* ***authRegister*** - Performs a registration and if successful, runs the `onRegistrationHook`

## Where Actions Can Be Used

Actions can be injected in many places:

* Most hook scripts
* Navigation Menu Items
* Form action elements (buttons)
* Named Actions
* `*_actions` schema keys (where supported)

## Adding Actions to a Page

To add actions to a page, you can use the `actions` key in the page schema. This key can be either an object (for running a single action) or an array of objects (to run a series of actions).

### Example: Adding a Button with Actions

Here's an example of how to add a button with actions to your page:

```json
{
  "actions": [{
    "action": "showAlert",
    "options": {
      "text": "This is the alert message text",
      "title": "Hello World",
      "type": "information"
    }
  }],
  "buttonClasses": "btn btn-primary",
  "styleClasses": "col-md-4",
  "text": "Show Alert",
  "type": "button"
}
```

### Example: Adding a Named Action

You can also use named actions to define a set of actions that can be called from multiple places. Here's an example of how to define a named action:

```json
"namedActions": {
  "showCantDoThat": {
    "action": "showAlert",
    "options": {
      "title": "Whoooaa!!",
      "text": "You can't do that thing you just did.",
      "type": "warning"
    }
  }
}
```

## Next Steps

You now have a basic understanding of how to add actions to your page in BetterForms. As you build out your first application, you'll become more familiar with these components and how to use them effectively.

Explore the [BetterForms Elements reference](/reference/components-overview) to see the wide variety of available elements and their specific configuration options.


# 2.7 Creating Your First List View

Now that you understand how to add elements and implement validation in your pages, let's explore how to create your first list view in BetterForms. This guide will walk you through the process of setting up and managing a list of rows in your application.

## Introduction to List Views

A list view in BetterForms is a dynamic way to display and manage multiple rows of data. Each row can consist of various fields, allowing you to create complex forms that handle multiple entries, such as address lists or contact details.

> **Note**: For more advanced table-based data display with features like sorting, filtering, and pagination, see [Working with Data Tables](/getting-started/ide-quick-tour/first-app/working-with-data-tables).

## When to Use List Views

List views are ideal for:

* Simple multi-row data entry forms
* Contact lists or address books
* Product lists with basic information
* Any scenario where you need to add/remove rows of similar data

For more complex data display needs (sorting, filtering, pagination), consider using [Data Tables](/getting-started/ide-quick-tour/first-app/working-with-data-tables) instead.

## Using the `listrows` Component

The `listrows` component is used to display and manage a list of rows where each row consists of fields defined in the schema. This allows for the dynamic rendering of forms that include multiple rows of data inputs.

### Example Configuration

Here's an example of how to configure a `listrows` component:

```json
[
    {
        "isCollapsable": false,
        "schema": {
            "fields": [
                {
                    "label": "Enter Some Contacts (min. 2 max 4)",
                    "max": 4,
                    "min": 2,
                    "model": "address",
                    "schema": {
                        "fields": [
                            {
                                "inputType": "text",
                                "label": "First Name",
                                "model": "nameFirst",
                                "styleClasses": "col-md-3",
                                "type": "input"
                            },
                            {
                                "inputType": "text",
                                "label": "Last Name",
                                "model": "nameLast",
                                "styleClasses": "col-md-3",
                                "type": "input"
                            },
                            {
                                "inputType": "text",
                                "label": "Street 1",
                                "model": "street1",
                                "styleClasses": "col-md-3",
                                "type": "input"
                            },
                            {
                                "inputType": "text",
                                "label": "Street 2",
                                "model": "street2",
                                "styleClasses": "col-md-3",
                                "type": "input"
                            }
                        ]
                    },
                    "styleClasses": "col-md-8",
                    "type": "listrows"
                }
            ]
        }
    }
]
```

### Key Attributes

* **`model`**: The data model path where the array of rows is stored. In this case, `address` is the model where each row of contact data is kept.
* **`min`**: Defines the minimum number of rows required (e.g., 2).
* **`max`**: Defines the maximum number of rows allowed (e.g., 4).
* **`schema.fields`**: An array of field configurations that will be displayed in each row. Each field can have its label, model, input type, and layout styling.

### Visual Example

* Each row contains four text input fields
* The \[+]\[-] buttons allow adding/removing rows
* The component enforces a minimum of 2 and maximum of 4 rows

## Best Practices

1. **Set Appropriate Limits**: Always define `min` and `max` values to prevent users from creating too many or too few rows.
2. **Use Consistent Styling**: Apply consistent `styleClasses` to maintain a clean layout.
3. **Validate Each Field**: Consider adding validation rules to individual fields within the rows.
4. **Consider Mobile Layout**: Use responsive classes (like `col-md-3`) to ensure the form looks good on all devices.

## Notes

* The `listrows` component is versatile and can handle different types of input fields like text, checkboxes, and dropdowns.
* The display of Add and Delete icons is automatically handled based on the defined `min` and `max` values.
* Each row's data is stored as an object in the array specified by the `model` attribute.

## Next Steps

You now have a basic understanding of how to create a list view in BetterForms. As you build out your first application, you'll become more familiar with these components and how to use them effectively.

For more advanced data display needs, you can explore:

* [Working with Data Tables](/getting-started/ide-quick-tour/first-app/working-with-data-tables) for complex table-based data display
* [BetterForms Elements reference](/reference/components-overview) for all available components


# 2.8 Understanding Page Data Flow

In BetterForms, the **Page Data Model** and **Hooks** are fundamental to how data flows within your application. This guide will help you understand how these components work together to manage data on your page.

## The Page Data Model

The **Data Model** is a JSON structure that stores all data accessible to the current page. It serves as the foundation for data binding, ensuring that elements like inputs and dynamic content are connected to the underlying data.

### Binding Data to Page Elements

For instance, if your data model includes:

```json
"user": {
  "firstName": ""
}
```

You can bind this data to an input field using the following snippet:

```json
{
  "inputType": "text",
  "label": "First Name",
  "model": "user.firstName",
  "styleClasses": "",
  "type": "input"
}
```

Or, in an HTML content field:

```html
<input type="text" v-model="model.user.firstName" />
<p>Your first name is: {{ model.user.firstName }}</p>
```

These examples show how the data model connects to elements on the page, allowing for dynamic interaction and updates.

### Default and Development Data Model

* **Default Data Model:** This contains the production data that will be loaded into the page's model when the page is first rendered. This data is sent to your FMS via the `onFormRequest` hook.
* **Development Data Model:** This is for data used only in the BetterForms IDE. The development data is merged with the default data model during previews, allowing you to work with mock data while building the page.

## Hooks and Data Flow

Server hooks are FileMaker-side scripts that BetterForms can trigger during page workflows. Client-side lifecycle named actions such as `onFormLoad` are related, but they are not FileMaker hooks.

### onFormRequest Hook

The `onFormRequest` hook is used to pull data from the backend and integrate it into the data model. This hook is triggered when the page is first loaded, allowing you to populate the data model with initial data from your FileMaker Server.

### onUtility Hook

The `onUtility` hook is the most common type of scoped hook and is called with the `runUtilityHook` action. This hook allows you to perform actions like saving data back to your FileMaker database.

### Related Lifecycle Named Actions

You may also use client-side named actions such as `onFormLoad` to start browser workflows after the page loads. Those workflows can include a `runUtilityHook` action when they need to call FileMaker.

### Controlling Data Sent from the Browser

To reduce data transfer and improve performance, you can control what data is sent from the browser to the server using methods like `modelFilterKeys` or `model` to filter the data model.

## Integration Settings

The **Integration** tab allows you to manage hooks and validation settings for your page:

* **Enable onFormRequest Hook:** When enabled, this will trigger the `onFormRequest` hook script. For most pages that have data coming from the backend, this should be ticked on.
* **Send Full Schema in Utility Hooks:** Sends the entire formSchema on hooks, providing more control but may increase transfer time on large schemas. Leave unchecked for better performance unless necessary.

## Next Steps

You now have a basic understanding of how the Page Data Model and Hooks work together to manage data flow within your BetterForms application. As you build out your first application, you'll become more familiar with these components and how to use them effectively.

Explore the [BetterForms Elements reference](/reference/components-overview) to see the wide variety of available elements and their specific configuration options.


# 2.9 Creating Your First Hook

Now that you understand the basics of the Page Data Model and Hooks, let's create your first hook in BetterForms. This guide will walk you through the process of setting up and running a simple hook.

## Introduction to Hooks

Hooks are scripts that insert themselves into normal workflow, allowing additional control. They can be triggered at various points in the page lifecycle, such as when the page is first loaded or when a utility action is performed.

Hooks fall into two categories:

* **Common Hooks:** These are not contextual to a specific page and can be called from anywhere. Examples include login or registration hooks.
* **Scoped Hooks:** These are bound to a specific page. The `onFormRequest` hook is an example of a scoped hook, which is called when a specific form loads.

## Creating Your First Hook

For your first hook, let's enable the `onFormRequest` option for the page you just created. This option can be enabled in the **Integration** tab of the page editor.

### Step 1: Enable the onFormRequest Hook

1. **Navigate to the Integration Tab:**
   * In the BetterForms IDE, open the page you want to add a hook to.
   * Go to the **Integration** tab.
2. **Enable the onFormRequest Hook:**
   * Find the **Enable onFormRequest Hook** option and toggle it on. This will trigger the `onFormRequest` hook script when the page is first loaded.

### Step 2: Locate the FileMaker Script

From the scripts you pasted into your legacy FileMaker file, locate the one called `Klai - onFormRequest - [scopedHookName]` where `[scopedHookName]` is the name of the scoped hook you defined on your page settings. This is the script that is run when the page is first loaded in the browser.

### Step 3: Modify the FileMaker Script

You can do anything you want in this script, but the ultimate goal for this example is to push data back to the browser. This is accomplished by setting the `$$BF_Model` global variable to a JSON object containing the data.

For now, just set the variable to something like this as an example, as long as it's different than what you set on the page's default data model so you can make sure the hook is running properly.

```javascript
JSONSetElement ( $$BF_Model ; 
    [ "nameFirst" ; "Jim" ; JSONString ];
    [ "nameLast" ; "Bob" ; JSONString ]
)
```

{% hint style="info" %}
Notice how we are only modifying the `$$BF_Model` JSON instead of replacing it. This is good practice when working with that global variable because otherwise you could accidentally erase existing data from the user's browser.
{% endhint %}

### Step 4: Run the Hook

* **Save the FileMaker script.**
* **Save your page settings.**
* **Refresh your preview tab (or launch it again).**

If everything is properly connected, you should see the data set from your FileMaker script on the page instead of the default data model. Even if you succeed here, be sure to review the troubleshooting tips for those times when it doesn't always go as planned.

## Troubleshooting Hooks

### Check the Inbox

Every single hook that interacts with your FileMaker server goes through the Helper file, so that's always a great place to start debugging.

Open the helper file and click the **Inbox** tab. Show the status toolbar and make sure you are viewing the most recent record. The left side shows you what data came in for a hook, while the right side shows you the data you sent back to the browser.

**If the record doesn't show up in the inbox**, then the hook never reached your FileMaker server.

* Double-check that the **onFormRequest** is enabled for your page in the Integration tab of the page editor. Then refresh your preview.
* Check the BetterForms [user credentials](/getting-started/ide-quick-tour/setup/configure-fm-server) *(see below)*

**If you're not sure if the record is in the inbox or not**, clear the inbox (delete all records) then refresh your preview in the browser to double-check if the inbox record was created.

**If the inbox record exists but you had unexpected results**, check the outbox payload (the right side) to see the data model that was returned to the browser. You can run the hook locally by clicking the **Run Hook** button.

**If the hook runs as expected locally, but not through BetterForms**, then you need to [check the privileges](/getting-started/ide-quick-tour/setup/configure-fm-server) of the BetterForms user. The only differences between how BetterForms runs the script and when you run it locally with that handy button is the user account that runs the script.

### Check Extended Privileges

Invalid credentials or misconfigured extended privileges for the BetterForms user is the most common cause of integration problems.

Make sure your credentials for the BetterForms user have XML and Data API privileges enabled, and that they match what was configured in your [site settings](/getting-started/ide-quick-tour/first-app/create-app).

* BetterForms encrypts these credentials so you would have to re-enter them to verify they are correct. Your current credentials are not viewable from the IDE.

## Next Steps

You now have a basic understanding of how to create and run your first hook in BetterForms. As you build out your first application, you'll become more familiar with these components and how to use them effectively.

Explore the [BetterForms Elements reference](/reference/components-overview) to see the wide variety of available elements and their specific configuration options.


# 2.10 Working with Data Tables

This page is a quick starting point for BetterForms data tables.

If you are deciding between list-style layouts and data tables:

* Use [Creating Your First List View](/getting-started/ide-quick-tour/first-app/creating-your-first-list-view) for simpler repeated record layouts.
* Use [Data Tables in the IDE](/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app/4.3-displaying-data-in-tables-page-builder-and-element-config) when you want sorting, filtering, pagination, row actions, or richer table presentation.

## Start Here

* [Data Table component reference](/reference/components-overview/common/tables2)
* [Data Tables in the IDE](/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app/4.3-displaying-data-in-tables-page-builder-and-element-config)
* [Vue-Tables-2 options reference](https://matanya.gitbook.io/vue-tables-2/options-api#options)

## Typical Use Cases

* admin and management screens
* reporting and analytics views
* searchable multi-column record lists
* row actions, details, and custom cell rendering

Use the reference page for supported BetterForms schema keys, then use the IDE guide for practical setup patterns.


# 3. Understanding Core BetterForms Concepts


# 3.1 Introduction to Hooks (and where to find them in the IDE)

## What is a Hook Script?

A hook script is a script that inserts itself into normal workflow allowing additional control. See: <https://en.wikipedia.org/wiki/Hooking>. You interact with your application and the web client via hooks scripts.

BetterForms hooks fall into 2 categories:

**Common hooks** are scripts that are not contextual to a specific page. Typically they can be called from anywhere and do not contain information about the user's current page. *(Eg: A login or registration hook are common hooks.)* Common hooks are unified by a Site and therefore configured in the site settings.

**Scoped hooks** are bound to a specific page. Most of the hook scripts you will use throughout BetterForms will be scoped hooks. Scoped hooks are unified by the form and therefore configured in the form editor.

## Where to Find Hooks in the BetterForms IDE

### Common Hooks

Common hooks are configured at the **Site level**:

1. Navigate to your **App (Site) Settings** in the BetterForms IDE
2. Go to the **Environment** section
3. Look for the **Named Actions** or **Hooks** tab
4. Here you can configure site-wide hooks like `onLogin`, `onRegistration`, etc.

### Scoped Hooks

Scoped hooks are configured at the **Page level**:

1. Open any **Page** in the BetterForms IDE
2. Navigate to the **Integration** tab
3. You'll find options like:
   * **Enable onFormRequest Hook** - triggers when the page loads
   * **Send Full Schema in Utility Hooks** - for data control
4. The **Scoped Hook Set Name** (set in Page Info) determines which FileMaker scripts will handle this page's hooks

## When do Hooks run?

A hook script can be triggered in a variety of places, here are just a few of the most common examples:

* **onLogin** - a common hook used when a user first logs in to the web client
* **onFormRequest** - a scoped hook that's optionally triggered when a page is first loaded
  * typically used to push data from FileMaker to the user
* using the [**runUtilityHook**](/reference/actions-processor/actions_overview/runutilityhook) action anywhere that [actions](/reference/actions-processor) can run
  * typically used on a button that's intended to save data back to your FileMaker database

Client-side lifecycle named actions such as `onAppLoad` and `onFormLoad` are documented separately. They can start browser workflows and may include `runUtilityHook`, but they are not FileMaker hook scripts.

For a full reference of all available hooks, see this page:

{% content-ref url="/pages/mrv6DkpGdbIHwUm5UiOM" %}
[Script Hooks](/reference/hooksoverview)
{% endcontent-ref %}

For client lifecycle named actions, see:

{% content-ref url="/pages/MHTiS34urb6JjeyIRv14" %}
[Lifecycle Hooks](/reference/hooksoverview/lifecycle-hooks)
{% endcontent-ref %}

## How to name Hooks

The next few steps will ask you to create names for both a common hook set and a scoped hook set. Here are few tips to keep in mind:

* The names of hooks are internal to your application and never displayed to the user
* It's best to keep them short but descriptive for easy legibility
* Hook names do **not** need to be unique throughout your site (see tip below)
* As long as the names match between BetterForms and FileMaker, everything will work just fine.

{% hint style="info" %}

#### **FileMaker Analogy**

A scoped hook name is referenced similarly to `Get ( LayoutName )` in FileMaker. When a script is run, the scoped hook name is used to let your server-side script know the context that the user is sitting in.

This allows you to group scripts that run from the context of the same page together in your FileMaker scripts. Since these name don't need to be unique, you can group multiple pages together by having them share the same scoped hook name and your FileMaker scripts will see them as effectively the same page.
{% endhint %}


# 3.2 Running Your First Hook (Practical Example)

Now that you understand the basics of hooks from the previous section, let's create your first hook in BetterForms. This guide will walk you through the practical process of setting up and running a simple hook.

## What You'll Accomplish

In this practical example, you'll:

* Enable the `onFormRequest` hook for a page
* Locate and modify the corresponding FileMaker script
* Push data from FileMaker to your browser
* Learn basic troubleshooting techniques

## Step-by-Step: Creating Your First Hook

### Step 1: Enable the onFormRequest Hook

The `onFormRequest` hook is triggered when a page is first loaded, making it perfect for your first hook example.

1. **Navigate to the Integration Tab:**
   * In the BetterForms IDE, open the page you want to add a hook to
   * Go to the **Integration** tab
2. **Enable the onFormRequest Hook:**
   * Find the **Enable onFormRequest Hook** option and toggle it **ON**
   * This will trigger the `onFormRequest` hook script when the page is first loaded

### Step 2: Locate the FileMaker Script

From the scripts you pasted into your FileMaker file (during helper file setup), locate the one called:

`Klai - onFormRequest - [scopedHookName]`

Where `[scopedHookName]` is the name of the scoped hook you defined in your page settings.

{% hint style="info" %}
**Script Naming:** The scoped hook name acts like a `Get ( LayoutName )` in FileMaker - it tells your script which page context it's running from.
{% endhint %}

### Step 3: Modify the FileMaker Script

You can do anything you want in this script, but for this example, the goal is to push data back to the browser. This is accomplished by setting the `$$BF_Model` global variable to a JSON object containing the data.

**Example Script Code:** Set the variable to something like this (make it different from your page's default data model so you can verify the hook is working):

```javascript
JSONSetElement ( $$BF_Model ; 
    [ "nameFirst" ; "Jim" ; JSONString ];
    [ "nameLast" ; "Bob" ; JSONString ]
)
```

{% hint style="info" %}
**Best Practice:** Notice how we are only **modifying** the `$$BF_Model` JSON instead of replacing it. This is good practice because otherwise you could accidentally erase existing data from the user's browser.
{% endhint %}

### Step 4: Test Your Hook

1. **Save the FileMaker script**
2. **Save your page settings** in the BetterForms IDE
3. **Refresh your preview tab** (or launch it again)

**Expected Result:** If everything is properly connected, you should see the data set from your FileMaker script on the page instead of the default data model.

## Troubleshooting Your First Hook

Even if you succeed, review these troubleshooting tips for when things don't go as planned:

### Check the Helper File Inbox

Every hook that interacts with your FileMaker server goes through the Helper file, making it the best place to start debugging.

1. **Open the Helper file** and click the **Inbox** tab
2. **Check for records:** The left side shows incoming data; the right side shows outgoing data
3. **Look at timestamps** to find your most recent hook call

**Common Scenarios:**

| **If this happens...**                        | **Then try this...**                                                                 |
| --------------------------------------------- | ------------------------------------------------------------------------------------ |
| **No record appears in inbox**                | Hook never reached FileMaker - check if onFormRequest is enabled, verify credentials |
| **Record exists but unexpected results**      | Check the outbox payload (right side) - data model returned to browser               |
| **Works locally but not through BetterForms** | Check user privileges - BetterForms user vs. your admin credentials                  |

### Check Extended Privileges

Invalid credentials or misconfigured extended privileges is the most common cause of integration problems.

**Verify your BetterForms user has:**

* **XML** extended privileges enabled
* **Data API** extended privileges enabled
* Credentials match what was configured in your site settings

{% hint style="warning" %}
**Credential Security:** BetterForms encrypts credentials so you can't view them from the IDE. You'll need to re-enter them to verify they're correct.
{% endhint %}

### Debug Using Helper File Tools

1. **Clear the inbox** (delete all records) to start fresh
2. **Refresh your preview** to generate a new hook call
3. **Use the "Run Hook" button** in the Helper file to test locally
4. **Compare local vs. remote results** to identify privilege issues

## Next Steps

Congratulations! You've successfully run your first hook. This foundation will serve you throughout your BetterForms development.

**What to explore next:**

* Try the `runUtilityHook` action (typically used with save buttons)
* Experiment with different data in `$$BF_Model`
* Learn about [BetterForms Actions](/getting-started/ide-quick-tour/3.-understanding-core-betterforms-concepts/3.3-introduction-to-actions-and-action-scripts-ide-context) to trigger hooks from buttons


# 3.3 Introduction to Actions & Action Scripts (IDE Context)

Now that you understand hooks, let's explore **Actions** - the instructions that tell BetterForms to perform operations like showing alerts, navigating to pages, or triggering hooks.

## What is an Action?

You can think of an action as similar to a single Script Step in FileMaker. When executed, some action will occur.

The most common actions are:

* **path** - change the URL of the browser to take them to another page
* **runUtilityHook** - run a hook script on your FileMaker server
* **showAlert** - display a non-intrusive message to the user

### What does an action look like?

An action is a JSON object that contains a key named "action". Most actions also contain an "options" key that define various parameters for the action.

**Example `runUtilityHook` action:**

```json
{
  "action": "runUtilityHook",
  "options": {
    "type": "save"
  }
}
```

**Example `showAlert` action:**

```json
{
  "action": "showAlert",
  "options": {
    "text": "This is the alert message text",
    "title": "Hello World",
    "type": "information"
  }
}
```

## Where to Find Actions in the BetterForms IDE

### Page-Level Actions (Local Scripts)

Actions are primarily managed at the **Page level**:

1. **Open any Page** in the BetterForms IDE
2. **Navigate to the Actions tab**
3. **Create new scripts** by clicking "+ New Script"
4. **Manage your actions** using the three-panel interface:
   * **Panel 1:** List of action scripts
   * **Panel 2:** Current script with drag-and-drop action arrangement
   * **Panel 3:** Edit individual action details

**Page Actions Features:**

* **JavaScript Editor:** Click the gutter / line number next to a `function` key to open the JavaScript editor
* **Test Actions:** Use the "Run" button to execute scripts immediately
* **Disable/Enable:** Temporarily disable scripts without deleting them
* **Priority:** Local scripts take priority over global scripts

### Site-Level Actions (Global Scripts)

Global actions are managed at the **App (Site) level**:

1. **Navigate to App Settings** in the BetterForms IDE
2. **Go to Environment section**
3. **Find the Scripts or Named Actions tab**
4. **Create global actions** that can be called from anywhere in your app

### Common Action Locations in Page Elements

Actions can be triggered from many places throughout your pages:

| **Element**      | **Action Key**      | **When Triggered**        |
| ---------------- | ------------------- | ------------------------- |
| **Buttons**      | `actions`           | When button is clicked    |
| **Input Fields** | `onChanged_actions` | When field value changes  |
| **Tables**       | `onRowClicked`      | When table row is clicked |
| **Page Load**    | `onFormLoad`        | When page first loads     |

## Types of Actions

### Regular Actions

* **showModal** - Renders a modal dialog
* **hideModal** - Hides a modal that is non-blocking
* **showAlert** - Renders a toaster style alert
* **path** - redirects the user to a new page
* **runUtilityHook** - runs the `onUtility` hook passing it params
* **clipboard** - runs `clipboard` action allowing interaction with the clipboard
* **cookie** - Allows setting of browser side cookies
* **wait** - Waits for specified time or event
* **emit** - Vue event bus message emit
* **scrollTo** - Scrolls to an element
* **namedAction** - Runs a named action (like calling a script)
* **function** - Runs JavaScript

### Authentication Actions

* **authLogin** - Performs an authentication login
* **authLogout** - Performs logout
* **authReset** - Performs a password reset action
* **authRegister** - Performs a registration

## Action Scripts (Named Actions)

**Named Actions** are reusable action sequences - like FileMaker scripts. They can be called from multiple places.

### Where to Define Named Actions

| **Scope**      | **Location in IDE**                  | **Usage**                            |
| -------------- | ------------------------------------ | ------------------------------------ |
| **Page-level** | Page → Actions tab                   | Only available on that specific page |
| **Global**     | App Settings → Environment → Scripts | Available throughout entire app      |

### Execution Priority

When a named action is called, BetterForms searches in this order:

1. **Page-level named actions** (local)
2. **Global named actions** (site-wide)

This allows you to create global defaults with page-specific overrides.

## Practical Example: Button with Actions

Here's how to add a button that shows an alert:

```json
{
  "type": "button",
  "text": "Click Me",
  "buttonClasses": "btn btn-primary",
  "actions": [
    {
      "action": "showAlert",
      "options": {
        "title": "Hello!",
        "text": "You clicked the button!",
        "type": "success"
      }
    }
  ]
}
```

## The Actions Queue

When actions are executed, they are added to the **actions queue** and processed sequentially. The queue is always running - when empty, it waits for new actions.

**Key Concepts:**

* Actions run **in order** (first in, first out)
* Some actions **block** until complete (like `showModal`)
* Others are **non-blocking** (like `showAlert`)

## Next Steps

You now understand how actions work and where to manage them in the BetterForms IDE.

**What to explore next:**

* Try creating a button with multiple actions in sequence
* Experiment with the `runUtilityHook` action to trigger FileMaker scripts
* Learn about [Page Data Model](/getting-started/ide-quick-tour/3.-understanding-core-betterforms-concepts/3.4-understanding-the-data-model-and-page-data-model-ui) to understand data flow

{% hint style="info" %}
**Complete Reference:** For detailed information about all available actions, see the [Actions Reference](/reference/actions-processor/actions_overview) documentation.
{% endhint %}


# 3.4 Understanding the Data Model (and Page Data Model UI)

In this final section of core concepts, you'll master the **Page Data Model** - the foundation of how your BetterForms pages store, display, and manipulate data.

## What is the Page Data Model?

The **Data Model** is a JSON structure that stores all data accessible to the current page. Think of it as the "variables" or "fields" available to your page - similar to how FileMaker fields store data, but more flexible.

**Key Characteristics:**

* **JSON Format:** All data is stored as a JSON object
* **Page-Specific:** Each page has its own data model
* **Dynamic:** Data can change through user interactions and hooks
* **Bindable:** Page elements connect to specific data model keys

### Simple Data Model Example

```json
{
  "user": {
    "firstName": "John",
    "lastName": "Doe",
    "email": "john@example.com"
  },
  "preferences": {
    "theme": "dark",
    "notifications": true
  }
}
```

## How Data Binding Works

Data binding connects page elements to your data model using the `model` property.

### Input Field Example

**Data Model:**

```json
{
  "user": {
    "firstName": ""
  }
}
```

**Page Element:**

```json
{
  "type": "input",
  "label": "First Name",
  "model": "user.firstName",
  "inputType": "text"
}
```

**Result:** The input field displays and updates the `user.firstName` value in real-time.

### HTML Display Example

You can also display data in HTML elements:

```html
<h1>Welcome, {{model.user.firstName}}!</h1>
<p>Your email is: {{model.user.email}}</p>
```

## Where to Find Data Model in the BetterForms IDE

### Data Model Tab (Primary Interface)

1. **Open any Page** in the BetterForms IDE
2. **Navigate to the Data Model tab**
3. **Two main sections available:**

| **Section**                | **Purpose**                              | **When Used**                                                    |
| -------------------------- | ---------------------------------------- | ---------------------------------------------------------------- |
| **Default Data Model**     | Production data loaded when page renders | Live application data sent to FileMaker via `onFormRequest` hook |
| **Development Data Model** | Mock data for IDE testing                | Preview and development in BetterForms IDE only                  |

### Page Builder Integration

The **Page Builder** tab works hand-in-hand with your data model:

1. **Schema Editor** - Define elements that bind to data model keys
2. **Real-time Preview** - See how your data model affects page rendering
3. **Snippets** - Use "HTML - Data Model Inspector" to debug data

**Data Model Inspector Snippet:**

```json
{
  "type": "html",
  "label": "Data Model Inspector",
  "html": "<pre>{{model}}</pre>",
  "styleClasses": "col-md-12"
}
```

## Data Model Lifecycle

Understanding when and how data flows is crucial:

### 1. Page Load

* **Default Data Model** loads first
* **onFormRequest Hook** (if enabled) can override with FileMaker data
* **Development Data Model** merges for IDE previews only

### 2. User Interaction

* Input fields update data model in real-time
* Changes are reflected immediately in bound elements
* No automatic saving - requires explicit actions

### 3. Data Persistence

* Use `runUtilityHook` action to send data to FileMaker
* FileMaker scripts receive the current data model
* Server can modify and return updated data

## Development vs Production Data

### Development Data Model

**Purpose:** Testing and development without affecting live data

**Features:**

* **IDE Only:** Never sent to FileMaker server
* **Merged with Default:** Combines with default data during previews
* **Caching:** Can be synced across IDE sessions

**Example Use Cases:**

```json
{
  "user": {
    "firstName": "Test User",
    "lastName": "Developer",
    "role": "admin"
  },
  "debugMode": true
}
```

### Default Data Model (Production)

**Purpose:** Real application data used in live environment

**Features:**

* **FileMaker Integration:** Sent to `onFormRequest` hook
* **User Data:** Updated through page interactions
* **Persistence:** Saved via `runUtilityHook` actions

## Data Model Best Practices

### 1. Structure Your Data Logically

**Good:**

```json
{
  "user": {
    "personal": { "firstName": "", "lastName": "" },
    "contact": { "email": "", "phone": "" }
  },
  "settings": { "theme": "light", "notifications": true }
}
```

**Avoid:**

```json
{
  "firstName": "",
  "lastName": "",
  "email": "",
  "phone": "",
  "theme": "",
  "notifications": ""
}
```

### 2. Use Meaningful Key Names

* Use `user.firstName` instead of `fn`
* Use `invoice.items` instead of `data`
* Be consistent with naming conventions

### 3. Plan for FileMaker Integration

Structure your data model to match how FileMaker will process it:

```json
{
  "record": {
    "id": "",
    "name": "",
    "created": ""
  },
  "metadata": {
    "lastModified": "",
    "version": ""
  }
}
```

## Common Data Model Patterns

### Master-Detail Pattern

```json
{
  "invoice": {
    "id": "INV-001",
    "customer": "Acme Corp",
    "total": 0
  },
  "lineItems": [
    { "product": "Widget A", "qty": 2, "price": 10.00 },
    { "product": "Widget B", "qty": 1, "price": 15.00 }
  ]
}
```

### Form Wizard Pattern

```json
{
  "step1": { "personal": { "firstName": "", "lastName": "" }},
  "step2": { "contact": { "email": "", "phone": "" }},
  "step3": { "preferences": { "newsletter": false }},
  "wizardState": { "currentStep": 1, "completed": false }
}
```

## Troubleshooting Data Model Issues

### Data Not Displaying

1. **Check binding:** Verify `model` property matches data structure
2. **Inspect with HTML:** Use Data Model Inspector snippet
3. **Check console:** Look for JavaScript errors

### Data Not Saving

1. **Verify hook configuration:** Ensure `runUtilityHook` action is configured
2. **Check FileMaker script:** Verify script receives `$$BF_Model` correctly
3. **Test locally:** Use Helper File "Run Hook" button

## Next Steps

Congratulations! You now understand all three core BetterForms concepts:

✅ **Hooks** - Connect to FileMaker scripts\
✅ **Actions** - Control application behavior\
✅ **Data Model** - Manage page data and binding

**Ready to build:** You can now create sophisticated BetterForms applications with confidence.

**What's next:** Explore [Common Customizations](https://github.com/DelfsEngineering/fm-betterforms/blob/master/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app) to learn practical implementation patterns.

{% hint style="info" %}
**Pro Tip:** The Data Model Inspector is your best friend for debugging. Add it to any page when troubleshooting data flow issues.
{% endhint %}


# 4. Common Customizations & Expanding Your App


# 4.1 Adding & Configuring Buttons (Page Builder)

Buttons are one of the most essential and versatile elements in BetterForms. They serve as the primary way users interact with your application by triggering actions like saving data, navigating pages, or running FileMaker scripts.

## What Makes BetterForms Buttons Powerful?

Unlike simple HTML buttons, BetterForms buttons can:

* **Run namedActions actions** Kike FileMaker script steps
* **Trigger FileMaker scripts** via hooks
* **Navigate between pages** with data
* **Show alerts and modals** for user feedback
* **Execute JavaScript** for advanced interactions

## Where to Add Buttons in the BetterForms IDE

### Using the Page Builder

1. **Open your page** in the BetterForms IDE
2. **Navigate to the Page Builder tab**
3. **Locate the Snippets section** (usually on the left side)
4. **Search for "button"** in the snippets library
5. **Copy and paste** the button JSON into your page schema's `fields` array

### Basic Button Structure

Every button in BetterForms follows this JSON structure:

```json
{
  "type": "button",
  "text": "Button Text",
  "buttonClasses": "btn btn-primary",
  "styleClasses": "col-md-6",
  "actions": [
    // Actions go here (namedAction by default)
  ]
}
```

**Key Properties:**

* `type`: Always "button"
* `text`: The label displayed on the button
* `buttonClasses`: Bootstrap CSS classes for button styling
* `styleClasses`: Layout classes (column width, spacing, etc.)
* `actions`: Array of actions to execute when clicked

## Common Button Types and Examples

### 1. Save Button (Triggers FileMaker Script)

This is the most common button type for FileMaker developers:

```json
{
  "type": "button",
  "text": "Save Record",
  "buttonClasses": "btn btn-success",
  "styleClasses": "col-md-4",
  "icon": "fa fa-save",
  "actions": [{
    "action": "runUtilityHook",
    "options": {
      "type": "save"
    }
  }]
}
```

**How it works:**

* Triggers the scoped FileMaker `onUtility` server hook via `runUtilityHook`
* Passes `type: "save"` as a parameter
* Your FileMaker script receives the current page data model

### 2. Navigation Button (Go to Another Page)

```json
{
  "type": "button",
  "text": "Go to Dashboard",
  "buttonClasses": "btn btn-info",
  "styleClasses": "col-md-4",
  "actions": [{
    "action": "path",
    "options": {
      "path": "/dashboard"
    }
  }]
}
```

**IDE Context:** The path corresponds to navigation slugs you set in App Settings → Navigation.

### 3. Alert Button (Show User Feedback)

```json
{
  "type": "button",
  "text": "Show Message",
  "buttonClasses": "btn btn-warning",
  "styleClasses": "col-md-4",
  "actions": [{
    "action": "showAlert",
    "options": {
      "title": "Success!",
      "text": "Your data has been saved successfully.",
      "type": "success"
    }
  }]
}
```

**Alert Types:** `success`, `error`, `warning`, `info`

### 4. Modal Button (Show Detailed Information)

```json
{
  "type": "button",
  "text": "Show Details",
  "buttonClasses": "btn btn-primary",
  "styleClasses": "col-md-4",
  "actions": [{
    "action": "showModal",
    "options": {
      "title": "Record Details",
      "body": "This modal shows detailed information about the selected record.",
      "icon": "info"
    }
  }]
}
```

### 5. External Link Button

```json
{
  "type": "button",
  "text": "View Documentation",
  "buttonClasses": "btn btn-secondary",
  "styleClasses": "col-md-4",
  "actions": [{
    "action": "path",
    "options": {
      "url": "https://docs.fmbetterforms.com/"
    }
  }]
}
```

## Advanced Button Configurations

### Multiple Actions in Sequence

Buttons can execute multiple actions one after another:

```json
{
  "type": "button",
  "text": "Save and Close",
  "buttonClasses": "btn btn-success",
  "styleClasses": "col-md-4",
  "actions": [
    {
      "action": "runUtilityHook",
      "options": {
        "type": "save"
      }
    },
    {
      "action": "showAlert",
      "options": {
        "title": "Saved!",
        "text": "Record saved successfully.",
        "type": "success"
      }
    },
    {
      "action": "path",
      "options": {
        "path": "/dashboard"
      }
    }
  ]
}
```

### Dropdown Buttons

Create dropdown menus with multiple options:

```json
{
  "type": "button",
  "text": "Actions",
  "buttonClasses": "btn btn-info",
  "styleClasses": "col-md-4",
  "icon": "fa fa-cog",
  "subs": [
    {
      "text": "Edit Record",
      "actions": [{
        "action": "path",
        "options": {
          "path": "/edit"
        }
      }]
    },
    {
      "divider": true
    },
    {
      "text": "Delete Record",
      "actions": [{
        "action": "runUtilityHook",
        "options": {
          "type": "delete"
        }
      }]
    }
  ]
}
```

### Button Groups

Group related buttons together:

```json
{
  "type": "button",
  "styleClasses": "col-md-8",
  "groupClasses": "btn-group",
  "group": [
    {
      "type": "button",
      "text": "Save",
      "buttonClasses": "btn btn-success",
      "actions": [{
        "action": "runUtilityHook",
        "options": {
          "type": "save"
        }
      }]
    },
    {
      "type": "button",
      "text": "Cancel",
      "buttonClasses": "btn btn-secondary",
      "actions": [{
        "action": "path",
        "options": {
          "path": "/cancel"
        }
      }]
    }
  ]
}
```

## Button Styling and Layout

### Bootstrap Button Classes

Use these `buttonClasses` for different button styles:

| Class               | Appearance             |
| ------------------- | ---------------------- |
| `btn btn-primary`   | Blue primary button    |
| `btn btn-success`   | Green success button   |
| `btn btn-danger`    | Red danger button      |
| `btn btn-warning`   | Yellow warning button  |
| `btn btn-info`      | Light blue info button |
| `btn btn-secondary` | Gray secondary button  |

### Size and Layout Classes

**Button Sizes:**

* `btn-lg` - Large button
* `btn-sm` - Small button
* `btn-block` - Full-width button

**Layout Classes (styleClasses):**

* `col-md-6` - Half width
* `col-md-4` - One-third width
* `col-md-12` - Full width

### Adding Icons

Icons enhance button usability:

```json
{
  "type": "button",
  "text": "Save",
  "icon": "fa fa-save",
  "buttonClasses": "btn btn-success",
  "styleClasses": "col-md-4"
}
```

**Common Font Awesome Icons:**

* `fa fa-save` - Save icon
* `fa fa-edit` - Edit icon
* `fa fa-trash` - Delete icon
* `fa fa-plus` - Add icon
* `fa fa-home` - Home icon

## Integration with FileMaker

### Passing Data to FileMaker Scripts

When using `runUtilityHook`, the current page data model is automatically sent to your FileMaker script:

**BetterForms Button:**

```json
{
  "actions": [{
    "action": "runUtilityHook",
    "options": {
      "type": "createContact",
      "priority": "high"
    }
  }]
}
```

**FileMaker Script Access:**

```javascript
// In your FileMaker onUtility hook script:
$type = JSONGetElement($$BF_Payload, "type")  // "createContact"
$priority = JSONGetElement($$BF_Payload, "priority")  // "high"
$modelData = JSONGetElement($$BF_Payload, "model")  // Complete page data
```

### Conditional Buttons

Show buttons based on data conditions:

```json
{
  "type": "button",
  "text": "Edit",
  "buttonClasses": "btn btn-primary",
  "styleClasses": "col-md-4",
  "visible_calc": "model.user.role === 'admin'",
  "actions": [{
    "action": "path",
    "options": {
      "path": "/edit"
    }
  }]
}
```

## Best Practices for Button Implementation

### 1. **Use Descriptive Text**

* ✅ "Save Contact" instead of "Save"
* ✅ "Delete Record" instead of "Delete"

### 2. **Provide Visual Feedback**

* Use appropriate button colors (success for save, danger for delete)
* Include relevant icons
* Show loading states for long operations

### 3. **Consider User Experience**

* Group related actions together
* Use confirmation dialogs for destructive actions
* Provide clear navigation paths

### 4. **Test Button Actions**

Use the **Actions tab** in the page editor to test button actions before deploying.

## Troubleshooting Common Issues

### Button Not Responding

* Check that the `actions` array is properly formatted
* Verify hook names match your FileMaker scripts
* Test actions individually in the Actions tab

### FileMaker Script Not Triggering

* Ensure the page is using the expected scoped hook set
* Check that your FileMaker script exists and is properly named
* Verify server connectivity in App Settings

### Styling Not Applied

* Check Bootstrap class names for typos
* Ensure proper JSON syntax with quotes
* Test with basic classes first

## Next Steps

Now that you understand buttons, you can:

* **Combine buttons with navigation** - See [4.2 Page Navigation](/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app/4.2-implementing-page-navigation-actions-and-site-navigation-ui)
* **Add buttons to data tables** - See [4.3 Displaying Data in Tables](/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app/4.3-displaying-data-in-tables-page-builder-and-element-config)
* **Style your buttons** - See [4.4 Basic App Styling](/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app/4.4-basic-app-styling-site-styling-ui)

{% hint style="info" %}
**Live Examples:** Check out the [BetterForms button examples](https://app.fmbetterforms.com/#/apps/pages/edit?id=5251675D-4A4D-4FE1-AD35-5D5B038CA924) to see these concepts in action.
{% endhint %}


# 4.2 Implementing Page Navigation (Actions & Site Navigation UI)

Navigation is a crucial aspect of any BetterForms application. This section covers both programmatic navigation using actions and setting up site-wide navigation menus that users can interact with.

## Two Types of Navigation in BetterForms

### 1. **Action-Based Navigation**

Navigation triggered by user actions (button clicks, form submissions, etc.)

### 2. **Site Navigation Menu**

Persistent navigation menus that appear on all pages (typically in the sidebar)

## Action-Based Navigation

### Using the `path` Action

The `path` action is the primary way to navigate between pages programmatically:

```json
{
  "action": "path",
  "options": {
    "path": "/dashboard"
  }
}
```

This action can be used in:

* **Button actions**
* **Form submissions**
* **Hook responses**
* **Navigation menu items**

### Navigation Button Examples

#### Basic Page Navigation

```json
{
  "type": "button",
  "text": "Go to Dashboard",
  "buttonClasses": "btn btn-primary",
  "styleClasses": "col-md-4",
  "actions": [{
    "action": "path",
    "options": {
      "path": "/dashboard"
    }
  }]
}
```

#### Navigation with Data

Pass data to the destination page using query parameters:

```json
{
  "type": "button",
  "text": "Edit Contact",
  "buttonClasses": "btn btn-info",
  "styleClasses": "col-md-4",
  "actions": [{
    "action": "path",
    "options": {
      "path": "/contact-edit?id=123&mode=edit"
    }
  }]
}
```

#### External Navigation

Link to external websites:

```json
{
  "type": "button",
  "text": "Open Documentation",
  "buttonClasses": "btn btn-secondary",
  "styleClasses": "col-md-4",
  "actions": [{
    "action": "path",
    "options": {
      "url": "https://docs.fmbetterforms.com/"
    }
  }]
}
```

### Save and Navigate Pattern

Common FileMaker developer pattern - save data then navigate:

```json
{
  "type": "button",
  "text": "Save & Return",
  "buttonClasses": "btn btn-success",
  "styleClasses": "col-md-4",
  "actions": [
    {
      "action": "runUtilityHook",
      "options": {
        "type": "save"
      }
    },
    {
      "action": "path",
      "options": {
        "path": "/contact-list"
      }
    }
  ]
}
```

## Setting Up Site Navigation in the IDE

### Where to Configure Site Navigation

1. **Open App Settings** in the BetterForms IDE
2. **Navigate to Environment section**
3. **Find the Navigation tab**
4. **Configure your navigation schema**

### Navigation Slugs (URL Mapping)

First, set up navigation slugs to map friendly URLs to page IDs:

```json
{
  "default": {
    "id": "36D74465-3F11-4CE4-A0FD-370D78C98B86"
  },
  "dashboard": {
    "id": "633D7D10-F8AD-4B6E-8D86-BDC8038D6E91"
  },
  "contacts": {
    "id": "A1B2C3D4-E5F6-7890-1234-567890ABCDEF"
  },
  "settings": {
    "id": "F1E2D3C4-B5A6-9876-5432-10FEDCBA9876"
  }
}
```

**Benefits of Navigation Slugs:**

* **Clean URLs:** `/dashboard` instead of `/form/633D7D10-F8AD-4B6E-8D86-BDC8038D6E91`
* **Easy Updates:** Change the page ID in one place to update all references
* **User-Friendly:** URLs are easier to understand and remember

### Navigation Menu Schema

The navigation menu is defined as an array of objects in the **Menu Schema** section:

```json
[{
  "sectionLabel": "Main Menu",
  "subs": [{
    "label": "Dashboard",
    "icon": "fa fa-home",
    "path": "/dashboard"
  }, {
    "label": "Contacts",
    "icon": "fa fa-users",
    "path": "/contacts"
  }, {
    "label": "Settings",
    "icon": "fa fa-cog",
    "path": "/settings"
  }]
}]
```

### Navigation Menu Structure

**Top Level Properties:**

* `sectionLabel`: Section title (not clickable)
* `subs`: Array of menu items
* `styleClassesSection`: CSS classes for the section

**Menu Item Properties:**

* `label`: Display text
* `icon`: Font Awesome icon class
* `path`: Navigation path (corresponds to slugs)
* `actions`: Alternative to path for complex behaviors
* `visible`: Boolean or `visible_calc` for conditional display

## Advanced Navigation Examples

### Dropdown Menus

Create hierarchical navigation with nested menus:

```json
[{
  "sectionLabel": "Main Menu",
  "subs": [{
    "label": "Dashboard",
    "icon": "fa fa-home",
    "path": "/dashboard"
  }, {
    "label": "Contacts",
    "icon": "fa fa-users",
    "subs": [{
      "label": "View All Contacts",
      "icon": "fa fa-list",
      "path": "/contacts"
    }, {
      "label": "Add New Contact",
      "icon": "fa fa-plus",
      "path": "/contact-new"
    }, {
      "divider": true
    }, {
      "label": "Import Contacts",
      "icon": "fa fa-upload",
      "path": "/contact-import"
    }]
  }]
}]
```

### Action-Based Navigation Items

Use actions instead of direct navigation for complex behaviors:

```json
[{
  "sectionLabel": "Main Menu",
  "subs": [{
    "label": "Logout",
    "icon": "fa fa-sign-out",
    "actions": [{
      "action": "authLogout"
    }, {
      "action": "path",
      "options": {
        "path": "/login"
      }
    }]
  }]
}]
```

### Conditional Navigation

Show navigation items based on user permissions:

```json
[{
  "sectionLabel": "Main Menu",
  "subs": [{
    "label": "Admin Panel",
    "icon": "fa fa-shield",
    "path": "/admin",
    "visible_calc": "app.user.role === 'admin'"
  }]
}]
```

### Navigation with Badges

Add dynamic badges to navigation items:

```json
[{
  "sectionLabel": "Main Menu",
  "subs": [{
    "html": "<a href=\"/#/messages\" title=\"Messages\">\n    <i class=\"fa fa-envelope\"></i> Messages\n    <span class=\"pull-right badge badge-danger\">{{app.unreadCount}}</span>\n</a>"
  }]
}]
```

## URL Structure and Query Parameters

### BetterForms URL Structure

```
https://yourdomain.fmbetterforms.com/#/dashboard?id=123&mode=edit
```

**Components:**

* `https://yourdomain.fmbetterforms.com` - Your domain
* `#/dashboard` - Navigation slug
* `?id=123&mode=edit` - Query parameters

### Working with Query Parameters

**In FileMaker Scripts:** Access query parameters via the `$$BF_Query` global variable:

```javascript
// In your onFormRequest hook
Set Variable $contactId = JSONGetElement($$BF_Query, "id")
Set Variable $mode = JSONGetElement($$BF_Query, "mode")
```

**In JavaScript/HTML:**

```javascript
// Using BetterForms utility function
const contactId = BF.getQueryParam('id');
const mode = BF.getQueryParam('mode');
```

## IDE Navigation Configuration Workflow

### Step 1: Plan Your Navigation Structure

1. **List all pages** in your app
2. **Group related pages** under logical sections
3. **Determine hierarchy** (which pages need submenus)
4. **Consider user roles** (which pages need conditional access)

### Step 2: Set Up Navigation Slugs

1. **Go to App Settings → Navigation**
2. **Find the Slug Names section**
3. **Add entries** for each page:

   ```json
   {
     "dashboard": { "id": "your-page-id" },
     "contacts": { "id": "your-page-id" },
     "settings": { "id": "your-page-id" }
   }
   ```

### Step 3: Configure Menu Schema

1. **In the Menu Schema section**
2. **Build your navigation structure**:

   ```json
   [{
     "sectionLabel": "Main Menu",
     "subs": [
       // Your menu items here
     ]
   }]
   ```

### Step 4: Test Navigation

1. **Save your changes**
2. **Test each navigation item**
3. **Verify paths resolve correctly**
4. **Check conditional visibility**

## Best Practices

### 1. **Consistent Navigation Structure**

* Keep navigation consistent across all pages
* Use familiar icons and terminology
* Group related functionality together

### 2. **User-Friendly URLs**

* Use descriptive navigation slugs
* Avoid exposing internal page IDs
* Keep URLs short and memorable

### 3. **Responsive Design**

* Consider mobile navigation experience
* Use appropriate icons for touch interfaces
* Test navigation on different screen sizes

### 4. **Performance Optimization**

* Minimize navigation complexity
* Use conditional visibility judiciously
* Consider caching navigation data

## Integration with FileMaker

### Navigation-Triggered Scripts

Navigation can trigger FileMaker scripts using actions:

```json
{
  "label": "Generate Report",
  "icon": "fa fa-file-pdf",
  "actions": [{
    "action": "runUtilityHook",
    "options": {
      "type": "generateReport",
      "reportType": "monthly"
    }
  }]
}
```

### Dynamic Navigation Updates

Update navigation based on FileMaker data:

```json
{
  "label": "Orders",
  "icon": "fa fa-shopping-cart",
  "path": "/orders",
  "visible_calc": "app.user.permissions.includes('view_orders')"
}
```

## Troubleshooting Common Issues

### Navigation Not Working

* Check that navigation slugs are properly configured
* Verify page IDs are correct
* Ensure JSON syntax is valid

### Pages Not Loading

* Confirm target pages exist
* Check page authentication settings
* Verify navigation paths match slug names

### Conditional Navigation Issues

* Test `visible_calc` expressions
* Verify app model data is available
* Check for JavaScript syntax errors

## Next Steps

Now that you understand navigation, you can:

* **Implement data tables** - See [4.3 Displaying Data in Tables](/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app/4.3-displaying-data-in-tables-page-builder-and-element-config)
* **Style your navigation** - See [4.4 Basic App Styling](/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app/4.4-basic-app-styling-site-styling-ui)
* **Learn about authentication** - See [Authentication](/reference/authentication)

{% hint style="info" %}
**URL Best Practice:** Always use navigation slugs instead of direct page IDs for better maintainability and user experience.
{% endhint %}


# 4.3 Displaying Data in Tables (Page Builder & Element Config)

BetterForms provides powerful options for displaying tabular data, from simple list views to advanced data tables. This section covers when to use each approach and how to implement them effectively.

## Two Main Approaches to Displaying Data

### 1. **List Views (`listrows`)**

* Simple multi-row data entry forms
* Add/remove rows functionality
* Good for data input scenarios
* Limited sorting and filtering

### 2. **Data Tables (`tables2`)**

* Advanced data display with sorting, filtering, pagination
* Read-only data presentation
* Complex interactions and custom formatting
* Based on Vue-Tables-2 library

## When to Use Each Approach

| **Use List Views When:**     | **Use Data Tables When:**              |
| ---------------------------- | -------------------------------------- |
| Users need to add/edit rows  | Displaying read-only data              |
| Simple data entry forms      | Large datasets requiring search/filter |
| Contact lists, address books | Reports and analytics                  |
| Product catalogs (editable)  | User management interfaces             |
| Max 20-30 rows               | Hundreds or thousands of rows          |

## List Views (`listrows`) Implementation

### Where to Add List Views in the IDE

1. **Open your page** in the BetterForms IDE
2. **Navigate to Page Builder tab**
3. **Search for "listrows"** in the snippets
4. **Copy and paste** into your page schema

### Basic List View Structure

```json
{
  "type": "listrows",
  "label": "Contact List",
  "model": "contacts",
  "min": 1,
  "max": 10,
  "styleClasses": "col-md-12",
  "schema": {
    "fields": [
      {
        "type": "input",
        "inputType": "text",
        "label": "First Name",
        "model": "firstName",
        "styleClasses": "col-md-4"
      },
      {
        "type": "input",
        "inputType": "text",
        "label": "Last Name",
        "model": "lastName",
        "styleClasses": "col-md-4"
      },
      {
        "type": "input",
        "inputType": "email",
        "label": "Email",
        "model": "email",
        "styleClasses": "col-md-4"
      }
    ]
  }
}
```

### Data Model for List Views

Your page's data model should include an array for the list data:

```json
{
  "contacts": [
    {
      "firstName": "John",
      "lastName": "Doe",
      "email": "john@example.com"
    },
    {
      "firstName": "Jane",
      "lastName": "Smith",
      "email": "jane@example.com"
    }
  ]
}
```

### Advanced List View Example

```json
{
  "type": "listrows",
  "label": "Product Inventory",
  "model": "products",
  "min": 0,
  "max": 50,
  "styleClasses": "col-md-12",
  "schema": {
    "fields": [
      {
        "type": "input",
        "inputType": "text",
        "label": "Product Name",
        "model": "name",
        "styleClasses": "col-md-3",
        "validator": "string"
      },
      {
        "type": "input",
        "inputType": "number",
        "label": "Price",
        "model": "price",
        "styleClasses": "col-md-2",
        "validator": "number"
      },
      {
        "type": "input",
        "inputType": "number",
        "label": "Quantity",
        "model": "quantity",
        "styleClasses": "col-md-2",
        "validator": "number"
      },
      {
        "type": "select",
        "label": "Category",
        "model": "category",
        "styleClasses": "col-md-3",
        "values": [
          { "value": "electronics", "label": "Electronics" },
          { "value": "clothing", "label": "Clothing" },
          { "value": "books", "label": "Books" }
        ]
      },
      {
        "type": "checkbox",
        "label": "Active",
        "model": "isActive",
        "styleClasses": "col-md-2"
      }
    ]
  }
}
```

## Data Tables (`tables2`) Implementation

### Where to Add Data Tables in the IDE

1. **Open your page** in the BetterForms IDE
2. **Navigate to Page Builder tab**
3. **Search for "table"** in the snippets
4. **Copy and paste** into your page schema

### Basic Data Table Structure

```json
{
  "type": "tables2",
  "label": "Customer List",
  "model": "customers",
  "styleClasses": "col-md-12",
  "columns": ["firstName", "lastName", "email", "status"],
  "options": {
    "filterable": true,
    "sortable": true,
    "pagination": {
      "enabled": true,
      "perPage": 10
    }
  }
}
```

### Data Model for Data Tables

Your page's data model should include an array of objects:

```json
{
  "customers": [
    {
      "firstName": "John",
      "lastName": "Doe",
      "email": "john@example.com",
      "status": "Active"
    },
    {
      "firstName": "Jane",
      "lastName": "Smith",
      "email": "jane@example.com",
      "status": "Inactive"
    }
  ]
}
```

### Advanced Data Table with Custom Formatting

```json
{
  "type": "tables2",
  "label": "Order Management",
  "model": "orders",
  "styleClasses": "col-md-12",
  "columns": ["orderNumber", "customer", "total", "status", "actions"],
  "options": {
    "filterable": ["customer", "status"],
    "sortable": ["orderNumber", "customer", "total"],
    "pagination": {
      "enabled": true,
      "perPage": 25
    },
    "texts": {
      "count": "Showing {from} to {to} of {count} orders",
      "filter": "Search orders:",
      "filterPlaceholder": "Type to search..."
    }
  },
  "slots": [
    {
      "slot": "total",
      "html": "<strong>${{props.row.total}}</strong>"
    },
    {
      "slot": "status",
      "html": "<span class=\"badge badge-{{props.row.status === 'Complete' ? 'success' : 'warning'}}\">{{props.row.status}}</span>"
    },
    {
      "slot": "actions",
      "html": "<button class=\"btn btn-sm btn-primary\" v-on:click=\"namedAction('editOrder', {orderId: props.row.id})\">Edit</button>"
    }
  ]
}
```

## Row Click Actions

### Adding Row Click Functionality

```json
{
  "type": "tables2",
  "label": "Customer List",
  "model": "customers",
  "styleClasses": "col-md-12",
  "columns": ["firstName", "lastName", "email"],
  "actions_onRowClick": [
    {
      "action": "path",
      "options": {
        "path_calc": "'/customer-detail?id=' + this.params.row.id"
      }
    }
  ]
}
```

### Using Named Actions with Row Data

```json
{
  "type": "tables2",
  "label": "Customer List",
  "model": "customers",
  "styleClasses": "col-md-12",
  "columns": ["firstName", "lastName", "email"],
  "actions_onRowClick": [
    {
      "action": "namedAction",
      "name": "viewCustomerDetail"
    }
  ]
}
```

**In your Named Action:**

```javascript
// Access the clicked row data
let customer = action.options.params.row;
let customerId = customer.id;
let customerName = customer.firstName + ' ' + customer.lastName;
```

## Custom Column Formatting with Slots

### Adding Custom HTML to Columns

```json
{
  "type": "tables2",
  "label": "Product Catalog",
  "model": "products",
  "columns": ["name", "price", "inventory", "status", "actions"],
  "slots": [
    {
      "slot": "price",
      "html": "<span class=\"text-success\">${{props.row.price}}</span>"
    },
    {
      "slot": "inventory",
      "html": "<span class=\"{{props.row.inventory < 10 ? 'text-danger' : 'text-success'}}\">{{props.row.inventory}} units</span>"
    },
    {
      "slot": "status",
      "html": "<span class=\"badge badge-{{props.row.status === 'active' ? 'success' : 'secondary'}}\">{{props.row.status}}</span>"
    },
    {
      "slot": "actions",
      "html": "<div class=\"btn-group\"><button class=\"btn btn-sm btn-primary\" v-on:click=\"namedAction('editProduct', {productId: props.row.id})\">Edit</button><button class=\"btn btn-sm btn-danger\" v-on:click=\"namedAction('deleteProduct', {productId: props.row.id})\">Delete</button></div>"
    }
  ]
}
```

### Child Row Expansion

Display additional details when a row is expanded:

```json
{
  "type": "tables2",
  "label": "Order List",
  "model": "orders",
  "columns": ["orderNumber", "customer", "total", "status"],
  "slots": [
    {
      "slot": "child_row",
      "html": "<div class=\"p-3\"><h5>Order Details</h5><p><strong>Shipping Address:</strong> {{props.row.shippingAddress}}</p><p><strong>Notes:</strong> {{props.row.notes}}</p></div>"
    }
  ]
}
```

## Integration with FileMaker

### Loading Data from FileMaker

Use the `onFormRequest` hook to populate table data:

**FileMaker Script:**

```javascript
// In your onFormRequest hook
If[Not IsEmpty($$BF_Query)]
  Set Variable [$action; Value:JSONGetElement($$BF_Query, "action")]
  If[$action = "loadCustomers"]
    // Perform find and return customer data
    Set Variable [$customers; Value:GetColumnAsArray("customers", "id", "1")]
    Set Variable [$result; Value:JSONSetElement("{}", "customers", $customers, JSONArray)]
    Exit Script [Result: $result]
  End If
End If
```

### Triggering FileMaker Actions from Tables

```json
{
  "type": "tables2",
  "label": "Customer List",
  "model": "customers",
  "columns": ["firstName", "lastName", "email", "actions"],
  "slots": [
    {
      "slot": "actions",
      "html": "<button class=\"btn btn-sm btn-success\" v-on:click=\"runUtilityHook({type: 'activateCustomer', customerId: props.row.id})\">Activate</button>"
    }
  ]
}
```

## Table Filtering and Sorting

### Basic Filtering

```json
{
  "type": "tables2",
  "model": "products",
  "columns": ["name", "category", "price"],
  "options": {
    "filterable": true,
    "filterByColumn": true,
    "pagination": {
      "enabled": true,
      "perPage": 20
    }
  }
}
```

### Custom Sort Functions

For numeric sorting of text fields:

```json
{
  "type": "tables2",
  "model": "products",
  "columns": ["name", "price", "date"],
  "options": {
    "sortable": true,
    "customSorting": {
      "price_function": "var ascending = arguments[0]; return function(a,b){if (ascending) return parseFloat(a.price) >= parseFloat(b.price) ? 1 : -1; return parseFloat(a.price) <= parseFloat(b.price) ? 1 : -1;}"
    }
  }
}
```

## Performance Considerations

### For Large Datasets

1. **Use Server-Side Pagination**
   * Load data in chunks from FileMaker
   * Implement search on the server side
2. **Optimize FileMaker Queries**
   * Use efficient finds
   * Limit returned fields
   * Use `getColumnAsArray` for better performance
3. **Consider Data Caching**
   * Cache frequently accessed data
   * Use development data model for testing

### Table Performance Tips

```json
{
  "type": "tables2",
  "model": "largeDataset",
  "columns": ["id", "name", "status"],
  "options": {
    "pagination": {
      "enabled": true,
      "perPage": 50
    },
    "filterable": ["name", "status"],
    "sortable": ["name", "id"],
    "texts": {
      "count": "Showing {from} to {to} of {count} records"
    }
  }
}
```

## Best Practices

### 1. **Choose the Right Component**

* Use `listrows` for data entry
* Use `tables2` for data display
* Consider user workflow needs

### 2. **Optimize User Experience**

* Provide clear column headers
* Use consistent formatting
* Add loading states for large datasets

### 3. **Handle Mobile Responsiveness**

* Test table behavior on mobile devices
* Consider hiding non-essential columns
* Use responsive design principles

### 4. **Security Considerations**

* Validate user permissions for row actions
* Sanitize data before display
* Use proper authentication for sensitive data

## Troubleshooting Common Issues

### List Views Not Showing

* Check that the data model contains the correct array
* Verify the `model` property matches your data structure
* Ensure `schema.fields` are properly configured

### Data Table Not Loading

* Verify the data model array structure
* Check that column names match data properties
* Ensure `model` property is correctly set

### Row Actions Not Working

* Check action syntax in `actions_onRowClick`
* Verify named actions are properly defined
* Test with simple actions first

## Next Steps

Now that you understand data display options, you can:

* **Style your tables** - See [4.4 Basic App Styling](/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app/4.4-basic-app-styling-site-styling-ui)
* **Integrate with authentication** - See [Authentication](/reference/authentication)
* **Explore advanced features** - See [Data Table Reference](/reference/components-overview/common/tables2)

{% hint style="info" %}
**Performance Tip:** For datasets with more than 100 rows, consider implementing server-side pagination and filtering in your FileMaker scripts.
{% endhint %}


# 4.4 Basic App Styling (Site Styling UI)

Styling is what transforms a functional BetterForms app into a polished, professional application. This section covers the fundamentals of styling your app using the Site Styling UI and Bootstrap CSS framework.

## BetterForms Styling Foundation

### Built on Bootstrap CSS

BetterForms is built with **Bootstrap 3.3.7** as the core UI framework, providing:

* **Responsive grid system** for layout
* **Pre-built components** (buttons, forms, tables)
* **Utility classes** for spacing, colors, and typography
* **Mobile-first design** principles

### Two Main Styling Approaches

1. **Bootstrap Classes** - Use existing Bootstrap classes
2. **Custom CSS** - Add your own styles for unique designs

## Where to Configure Styling in the IDE

### Site-Level Styling (Global)

1. **Open App Settings** in the BetterForms IDE
2. **Navigate to Environment section**
3. **Find styling-related tabs:**
   * **CSS** - Custom CSS styles
   * **Theme Settings** - Built-in themes and layout options
   * **Slots** - Header, footer, and other page sections
   * **DOM Header Insertions** - External CSS libraries

### Page-Level Styling (Individual Elements)

1. **Open any page** in the BetterForms IDE
2. **Navigate to Page Builder tab**
3. **Use `styleClasses` property** on individual elements

## Basic Element Styling

### Using Bootstrap Classes

Every element in BetterForms supports the `styleClasses` property:

```json
{
  "type": "input",
  "label": "Customer Name",
  "model": "customerName",
  "styleClasses": "col-md-6 col-sm-12"
}
```

### Common Bootstrap Layout Classes

**Column Widths:**

* `col-md-12` - Full width (12 columns)
* `col-md-6` - Half width (6 columns)
* `col-md-4` - One-third width (4 columns)
* `col-md-3` - One-quarter width (3 columns)

**Responsive Breakpoints:**

* `col-xs-*` - Extra small devices (phones)
* `col-sm-*` - Small devices (tablets)
* `col-md-*` - Medium devices (desktops)
* `col-lg-*` - Large devices (large desktops)

### Button Styling Classes

```json
{
  "type": "button",
  "text": "Save Record",
  "buttonClasses": "btn btn-success btn-lg",
  "styleClasses": "col-md-4 text-center"
}
```

**Button Classes:**

* `btn-primary` - Blue (default)
* `btn-success` - Green
* `btn-danger` - Red
* `btn-warning` - Yellow
* `btn-info` - Light blue
* `btn-secondary` - Gray

**Button Sizes:**

* `btn-lg` - Large
* `btn-sm` - Small
* `btn-xs` - Extra small

## Site-Wide CSS Customization

### Where to Add Custom CSS

1. **Go to App Settings → Environment → CSS**
2. **Add your custom CSS styles**
3. **Save changes**

### Basic Custom CSS Example

```css
/* Custom button style */
.btn-custom {
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 8px;
    padding: 10px 20px;
    font-weight: bold;
}

.btn-custom:hover {
    background-color: #0056b3;
    color: white;
}

/* Custom input styling */
.form-control-custom {
    border: 2px solid #007bff;
    border-radius: 6px;
    padding: 12px;
}

/* Custom container spacing */
.section-spacing {
    margin-top: 30px;
    margin-bottom: 30px;
}
```

### Using Custom CSS in Elements

```json
{
  "type": "button",
  "text": "Custom Button",
  "buttonClasses": "btn btn-custom",
  "styleClasses": "col-md-4"
}
```

## Page-Specific Styling

### Adding Page-Level Classes

You can add classes to entire pages for targeted styling:

```json
{
  "schema": {
    "styleClasses": "dashboard-page",
    "fields": [
      // Your page elements
    ]
  }
}
```

### Page-Specific CSS

```css
/* Styles that only apply to dashboard pages */
.dashboard-page .btn {
    border-radius: 12px;
}

.dashboard-page .form-control {
    border-color: #28a745;
}

.dashboard-page h1 {
    color: #007bff;
    font-weight: 300;
}
```

## Theme Settings and Layout Options

### Built-in Theme Options

1. **Navigate to App Settings → Environment → Theme Settings**
2. **Select from available themes:**
   * Default theme
   * Dark theme
   * Light theme
   * Custom color schemes

### Layout Control Options

**Navigation Options:**

* Show/hide left sidebar navigation
* Navigation position (top or side)
* Navigation width

**Header Options:**

* Show/hide header
* Header height
* Header background color

**Page Layout:**

* Container width
* Page background
* Content padding

## Advanced Styling with Slots

### What are Slots?

Slots are predefined areas where you can inject custom HTML and CSS:

* **Header slots** - Top of the page
* **Footer slots** - Bottom of the page
* **Navigation slots** - Within the navigation menu

### Adding Custom Header Content

1. **Go to App Settings → Environment → Slots**
2. **Select Header slot**
3. **Add your HTML content:**

```html
<div class="custom-header">
    <img src="{{app.logoURL}}" alt="Company Logo" class="logo">
    <h1>{{app.companyName}}</h1>
    <div class="user-info">
        Welcome, {{app.user.firstName}}!
    </div>
</div>
```

### Styling Slot Content

```css
.custom-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 20px;
    background-color: #f8f9fa;
    border-bottom: 1px solid #dee2e6;
}

.custom-header .logo {
    height: 40px;
    width: auto;
}

.custom-header h1 {
    margin: 0;
    font-size: 24px;
    color: #007bff;
}

.user-info {
    font-size: 14px;
    color: #6c757d;
}
```

## External Libraries and Fonts

### Adding External CSS Libraries

Use DOM Header Insertions to add external libraries:

1. **Go to App Settings → Environment → DOM Header Insertions**
2. **Load First section** for critical CSS:

```html
<!-- Custom fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">

<!-- Font Awesome icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">

<!-- Custom CSS framework -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
```

### Using Custom Fonts

```css
/* Apply custom font to entire app */
body {
    font-family: 'Inter', sans-serif;
}

/* Apply to specific elements */
.page-title {
    font-family: 'Inter', sans-serif;
    font-weight: 600;
    font-size: 28px;
}
```

## Responsive Design Considerations

### Mobile-First Approach

```json
{
  "type": "input",
  "label": "Customer Name",
  "model": "customerName",
  "styleClasses": "col-xs-12 col-sm-8 col-md-6 col-lg-4"
}
```

### Responsive Utilities

```css
/* Hide on mobile */
@media (max-width: 768px) {
    .hide-mobile {
        display: none;
    }
}

/* Stack buttons on mobile */
@media (max-width: 768px) {
    .btn-group .btn {
        display: block;
        width: 100%;
        margin-bottom: 10px;
    }
}
```

## Common Styling Patterns

### Card-Style Containers

```css
.card {
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    padding: 24px;
    margin-bottom: 24px;
}

.card-header {
    border-bottom: 1px solid #e9ecef;
    padding-bottom: 16px;
    margin-bottom: 16px;
}

.card-title {
    font-size: 18px;
    font-weight: 600;
    color: #212529;
    margin: 0;
}
```

### Form Styling

```css
.form-section {
    background: #f8f9fa;
    border-radius: 6px;
    padding: 20px;
    margin-bottom: 20px;
}

.form-section h3 {
    color: #495057;
    font-size: 16px;
    font-weight: 600;
    margin-bottom: 16px;
}

.form-group {
    margin-bottom: 16px;
}

.form-control {
    border-radius: 4px;
    border: 1px solid #ced4da;
    padding: 8px 12px;
}
```

### Status Indicators

```css
.status-badge {
    display: inline-block;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 500;
    text-transform: uppercase;
}

.status-active {
    background-color: #d4edda;
    color: #155724;
}

.status-pending {
    background-color: #fff3cd;
    color: #856404;
}

.status-inactive {
    background-color: #f8d7da;
    color: #721c24;
}
```

## Integration with FileMaker Data

### Dynamic Styling Based on Data

```json
{
  "type": "html",
  "html": "<div class=\"status-badge status-{{model.record.status}}\">{{model.record.status}}</div>",
  "styleClasses": "col-md-2"
}
```

### Conditional Styling

```json
{
  "type": "button",
  "text": "Edit",
  "buttonClasses_calc": "model.user.canEdit ? 'btn btn-primary' : 'btn btn-secondary'",
  "styleClasses": "col-md-2"
}
```

## Best Practices for App Styling

### 1. **Consistent Design System**

* Use a limited color palette
* Maintain consistent spacing
* Use the same fonts throughout
* Apply consistent button styles

### 2. **Performance Optimization**

* Minimize custom CSS
* Use external libraries judiciously
* Optimize images and assets
* Test loading times

### 3. **Mobile-First Design**

* Design for mobile first
* Use responsive breakpoints
* Test on various devices
* Consider touch interactions

### 4. **Accessibility**

* Ensure sufficient color contrast
* Use semantic HTML elements
* Provide alternative text for images
* Test with screen readers

## Common Styling Issues and Solutions

### Issue: Custom CSS Not Applying

**Solution:**

1. Check CSS syntax for errors
2. Ensure CSS is in the correct section (Site CSS)
3. Use browser developer tools to inspect elements
4. Check CSS specificity (be more specific)

### Issue: Responsive Layout Breaking

**Solution:**

1. Test with different screen sizes
2. Use proper Bootstrap grid classes
3. Avoid fixed widths
4. Test on real devices

### Issue: Styling Conflicts

**Solution:**

1. Use more specific CSS selectors
2. Check for conflicting Bootstrap classes
3. Use `!important` sparingly
4. Organize CSS logically

## Next Steps

Now that you understand basic styling, you can:

* **Explore advanced customization** - Custom components and themes
* **Learn about authentication** - See [Authentication](/reference/authentication)
* **Optimize performance** - See [Environment Deep Dive](/getting-started/ide-quick-tour/environment)

## Resources

* [Bootstrap 3.3.7 Documentation](https://getbootstrap.com/docs/3.3/)
* [CSS Grid Guide](https://css-tricks.com/snippets/css/complete-guide-grid/)
* [Responsive Design Basics](https://developers.google.com/web/fundamentals/design-and-ux/responsive)

{% hint style="info" %}
**Testing Tip:** Use your browser's developer tools to test CSS changes live before adding them to your site settings.
{% endhint %}


# 5. Mastering the BetterForms Environment & Advancing Your Skills

This advanced section takes you deeper into the BetterForms environment, focusing on sophisticated configurations, settings, and an in-depth look at page and application management. It also provides guidance on further learning paths.

**Key Goals for this Section:**

* Gain an in-depth understanding of managing different environments.
* Master page configurations, settings, and the Page Builder interface.
* Learn to work effectively with global scripts.
* Understand how to manage application-level (Site) settings and navigation.
* Discover resources and topics for continued learning and skill advancement.

By completing this material, you'll be well-equipped to manage complex BetterForms projects and continue to expand your expertise.

The articles below cover these advanced topics:


# 5.1 Understanding & Managing Environments (In-Depth)

In BetterForms, an "environment" represents a distinct instance of your application. The most common setup includes three environments:

* **Development:** Your primary workspace for building, testing, and debugging new features.
* **Staging:** A pre-production environment that mirrors your live setup, used for final testing and quality assurance before deploying to users.
* **Production:** The live version of your application that your end-users interact with.

Each environment is independent, with its own database connections, settings, and deployed version, allowing for a safe and organized development workflow.

## Managing Your Environment

Each environment has a dropdown menu (indicated by the ellipsis) that allows you to manage it:

<figure><img src="/files/6kGafggmrPmh5zHxA7BT" alt=""><figcaption></figcaption></figure>

* **Edit**: Modify the environment settings, such as its name, stage, domains, and credentials. You can lock the environment to prevent changes (usually for the production environment) and turn on the development tools (usually for the development environment).

  * **Lock Environment**: Prevent any changes to this environment by locking it.
  * **Development Tools**: Enable this option to view development tools such as app and model data on the site page.

  <figure><img src="/files/R7tc1GU5h2tUoJU8d4KJ" alt=""><figcaption></figcaption></figure>

  <figure><img src="/files/WHhteHZrb4NXs8VBnIdJ" alt=""><figcaption></figcaption></figure>
* **Deploy**: Move the current version of your app to the selected stage (Development, Staging, Production).

  <figure><img src="/files/XTa3iBFNW3bHwI3Agx5w" alt=""><figcaption></figcaption></figure>

  <figure><img src="/files/npMBVAtvkdYaBIIP304E" alt=""><figcaption></figcaption></figure>

  After a deployment completes, BetterForms can notify the live app that a new deployment is available. By default, the client shows a refresh-needed banner so users can reload safely.

  If your app defines the global named action `onDeployPost`, BetterForms runs that browser-side workflow instead of the shared fallback banner. This is available in **Editor v125+**. For the full behavior and payload details, see [Named Actions (Action Scripts)](/reference/actions-processor/actions_named).
* **Rollback**: Revert to a previous version of the environment if needed.

  <figure><img src="/files/cpSwYR3mEEEmuQgQp107" alt=""><figcaption></figcaption></figure>

By utilizing these options, you can effectively manage and maintain the different stages of your app's lifecycle, ensuring a smooth development and deployment process.


# 5.2 Deep Dive: Page Configuration & Settings

In the **Pages** tab, you'll see a list of your app's pages. Here, you can:

* **Create a new page**
* **Edit an existing page** from the list
* **Duplicate a page** using the ellipsis dropdown menu on the right side of each page row. This dropdown also includes options to **Preview** and **Delete** a page for your convenience.

Clicking on a page in the list redirects you to it. On the top header of the page, you can:

* **Edit the page name**
* **Preview** the page
* **Save** changes
* **Duplicate** or **Delete** the page
* **Rollback** to any previous version.


# Navigating the Page Builder Interface

### Page Schema

The Page Schema is the backbone of your app's pages, defining all elements that will appear. It is structured in JSON format and includes groups and fields that organize and detail these elements.

### \_note Property

The `_note` property (e.g., `"HEADER"`) is a user-defined label used for organizing the schema. It helps you structure and label different parts of your page without rendering anything on the actual page. This property can be applied to groups, fields, or any sub-contents.

### Groups and Fields

* <mark style="color:red;">**`"groups"`**</mark>: These organize fields into logical sections like "header," "body," or any custom section you create.
* <mark style="color:red;">**`"fields"`**</mark>: Fields contain the actual elements, such as HTML, buttons, or other components, that will appear on your page.

### Styling and Visibility of Elements

* <mark style="color:red;">**`"styleClasses"`**</mark>: This property applies Tailwind CSS or any custom styles defined in the DOM header insertions and CSS.
* **Dynamic Visibility**: Elements can have <mark style="color:red;">`"visible": false`</mark> for static rendering or <mark style="color:red;">`"visible_calc"`</mark> for dynamic rendering based on the data model. For example,

```json
"visible_calc": "model.isShowing"
```

### Input Field Validation

* **Validators**: Input fields can include validators to ensure data integrity. For example:
  * A simple type validator: <mark style="color:red;">`"validator": "string"`</mark>
  * A dynamic validator:

    ```json
    {
      "validator": "calc",
      "validator_calc": "model.user.firstName"
    }
    ```
  * **Error Messages**: You can set an error message with the `"errorMsg"` key
* **Triggering Validation**: For HTML content input fields, you need a button or an `@change` event to trigger a `namedAction`. Then, copy the `validate` action snippet from the library:

  ```json
  {
      "action": "validate"
  }
  ```

  If you need custom failure handling, use the `onFailed` callback described in the [`validate` action reference](/reference/actions-processor/actions_overview/validate).

### Using Snippets

To add a snippet from the Snippet Library, copy the desired snippet and paste it in your Page Schema. This allows you to quickly add elements like buttons or custom HTML to your page.

### Managing JSON Structure

In your editor, you can use the top-right toolbar to fold or unfold page schema objects or nested elements at different levels. This feature helps you manage and navigate through complex JSON structures. Additionally, you have the option to format the JSON, ensuring that the code is neatly organized and easy to read—especially useful for large and detailed page schemas.

### Editing HTML Content

When working with HTML content within the Page Schema, clicking on the line number of an HTML field will open an HTML editor. This allows you to directly edit the HTML code in a more user-friendly interface, making it easier to customize your content and layout.

#### HTML Editor Features

In the HTML Editor, you can:

* **Edit HTML content**: Directly modify HTML elements and see real-time updates.
* **Edit Data Models**: Modify both the main data model and the development data model JSON.
* **Toggle DEV Mode**: Use the DEV button in the top right corner to enable or disable the development data model.
* **AI Assistant**: You can ask the AI assistant for help, and it will automatically modify the HTML content based on your requests. For external IDE workflows, see [Klai Studio MCP (Beta)](/reference/apis-and-services/integrations/klai-studio-mcp).

Remember to **save** your changes before closing the HTML Editor\*\*.\*\* After that, be sure to **save** the entire page to ensure all modifications are stored.

5. **Save Button**:
   * Saves all changes made to the page configuration, including the schema, data model, and other settings. Remember to save frequently.


# Working with Page-Level Action Scripts

This tab is for managing <mark style="color:red;">local actions</mark> within the current page. You can:

* Create new scripts by clicking "+ New Script" in the first panel.
* The second panel shows your current script, where you can:
  * Add actions
  * Rearrange actions via drag-and-drop
* Edit action details in the third panel.
* **JavaScript Editor**: If you have a `"function"` action, click the gutter / line number next to the `function` key to open the JavaScript editor.
* **Disabled**: Temporarily disable a script without deleting it.
* **Run**: Execute the script immediately for testing purposes.
* **Delete**: Permanently remove the script.

By organizing your logic into named scripts, you can create more modular, reusable, and maintainable actions within your BetterForms pages.

For example, when a `function` action is selected in the script editor, clicking the line number next to the `function` key opens the JavaScript editor modal:

<figure><img src="/files/mfFF2mSLk6tfB9S91WLP" alt="Click the line number next to the function key to open the JavaScript editor"><figcaption></figcaption></figure>

{% hint style="info" %}
**Priority of Local Scripts**: Local scripts take priority over global scripts. For example, if both local and global scripts define a namedAction `"someAction"`, the local script will be executed on this page.
{% endhint %}


# Configuring the Page Data Model

The **Data Model** tab contains all data accessible to the current page in JSON format.

### Understanding the Data Model

The data model is a JSON structure that stores data used by elements on your page. It serves as the foundation for data binding, ensuring that elements like inputs and dynamic content are connected to the underlying data.

### Binding Data to Page Elements

For instance, if your data model includes:

```json
"user": {
  "firstName": ""
}
```

You can bind this data to an input field using the following snippet:

```json
{
  "inputType": "text",
  "label": "First Name",
  "model": "user.firstName",
  "styleClasses": "",
  "type": "input"
}
```

Or, in an HTML content field:

```html
<input type="text" v-model="model.user.firstName" />
<p>Your first name is: {{ model.user.firstName }}</p>
```

These examples show how the data model connects to elements on the page, allowing for dynamic interaction and updates.

### Default Data Model

This editor contains the production data that will be loaded into the page's model when the page is first rendered. This data is sent to your FMS via the <mark style="color:red;">`onFormRequest`</mark> hook.

### Development Data Model

This editor is for data used only in the BetterForms IDE. The development data is merged with the default data model during previews, allowing you to work with mock data while building the page.

#### Caching and Syncing

In the **Models** tab, each key can be configured under **Key Caching**:

* **Cache in browser** = local storage
* **Cache in tab** = session storage
* **Sync with app** = keep that page model key synced with the global app model key

This is configured directly in the UI (you do not need to edit JSON paths manually in this screen).

![Key Caching with Sync with app enabled](/files/6tVZfGztUKOF6dMfiZpY)

Example of a page model key (`pet`) that has **Sync with app** enabled:

![Default Data Model with synced key](/files/YroJiRAuRg0yy4KqV7Tv)

For detailed app model behavior and caching notes, see [App Model](/reference/site-settings/app-model).

{% hint style="warning" %}
**When returning both App + Model from a hook:**

If a key is set to **Sync with app** and your hook returns that same key in both `$$BF_App` and `$$BF_Model`, the **app value is authoritative** for that key.

Use `$$BF_App` for the final shared value you want synced across pages.
{% endhint %}


# Page Integration Settings Explained

The **Integration** tab allows you to manage hooks and validation settings for your page.

#### **General Hooks**

* **Enable onFormRequest Hook**: When enabled, this will trigger the `onFormRequest` hook script. For most pages that have data coming from the backend, this should be ticked on. The data will be integrated into the data model and will overwrite the development data (if present). For more details on hooks, you can reference this page: [Running Your First Hook](/getting-started/ide-quick-tour/first-app/creating-your-first-hook).
* **Send Full Schema in Utility Hooks**: Sends the entire formSchema on hooks, providing more control but may increase transfer time on large schemas. Leave unchecked for better performance unless necessary.

#### **Validation**

* **Validate After Loaded**: Triggers validation routines as soon as the page is loaded. This should be enabled if any fields on the page have validators.
* **Validate After Changed**: Triggers validation routines as soon as a field is changed. This should also be enabled if fields have validators to ensure data integrity.


# Managing Page Info & Other Settings

The **Page Info** tab contains essential settings and metadata for your page.

* **UUID**: A unique identifier for the page.
* **Description**: You can add a description for internal reference (not displayed to the user).
* **Scoped Hook Set Name**: Specify the hook set name for scoped hooks. [Learn more about hooks](/getting-started/ide-quick-tour/3.-understanding-core-betterforms-concepts/3.1-introduction-to-hooks-and-where-to-find-them-in-the-ide).
* **Authentication**: Toggle authentication requirements for the page.
* **Delete Page**: Permanently delete the page.

#### **Misc Form Settings**

All settings for your page are defined in JSON format under Misc Form Settings. This includes:

* **namedActions**: These are local scripts defined in JSON, specifying action like <mark style="color:red;">`onFormLoad`</mark>. While it's recommended to edit your scripts and actions in the **Actions** tab, you can also manage them directly in JSON format here.
* **Style Classes**: Customize the styling for the body and page using `styleClassesBody` and `styleClassesPage`.
* **Other Settings**: Control behaviors like `requestHook`, `sendFullSchema`, and `validateOnBack`. These settings correspond to options available in the **Integration** tab, such as `onFormRequest` and `Send Full Schema in Utility Hooks`. Changes made here will affect the behavior as if they were toggled in the Integration tab.


# 5.3 Working with Global Scripts

This page is for managing <mark style="color:red;">global actions</mark> within your app. You can:

* Create new scripts by clicking "+ New Script" in the first panel.
* The second panel shows your current script, where you can:
  * Add actions
  * Rearrange actions via drag-and-drop
* Edit action details in the third panel.
* Remember to save changes.
* In the top-right corner, switch to the **JSON Editor** to work directly with JSON.
* Next to the Save button, use the ellipsis to access the **Roll Back** option for reverting to previous versions.


# 5.4 Managing App (Site) Settings & Navigation

### **Slug Names**

Set the slug or path for each page.

* **Slug:** A slug is the part of the URL that identifies a particular page on your site in a more readable way, typically following the domain (e.g., `subdomain.domain.com/`<mark style="color:red;">`your-slug`</mark>).
* **(internal) Path:** The full URL path, which can include the slug and any additional routing information (e.g., `subdomain.domain.com`<mark style="color:red;">`/your-slug`</mark>).

Remember to save your changes. In the ellipsis dropdown, you can:

* **Redirect** to edit the page
* **Precache** the page (sync data with the app)
* **Add more slugs** for the page
* **Preview** the page
* **Delete** the page

### **Menu Schema**

**Sidebar Navigation Menu Schema:** This section allows you to edit the structure of your sidebar navigation menu. The schema is represented as an array of objects, for example:

```json
[{
    "sectionLabel": "Menu",
    "subs": [{
        "exact": true,
        "icon": "fa fa-home",
        "label": "Home",
        "path": "/"
    }]
}]
```

* Each object within the <mark style="color:red;">`subs`</mark> array represents a menu item.
* You can customize each menu item by:
  * Adding your menu text as the **label**
  * Specifying an **icon** to display before the text
  * Defining the **path** of the page the menu item links to
* You can also apply custom styling with the <mark style="color:red;">`"styleClasses"`</mark> property and control dynamic rendering using <mark style="color:red;">`"visible_calc"`</mark>.
* **HTML objects** can be included inside <mark style="color:red;">`subs`</mark> as menu items.
* **Nested menus** can be created by using nested <mark style="color:red;">`subs`</mark> arrays to manage hierarchical navigation.


# 5.5 Exploring Further: What to Learn Next

You've got the basics down! Now it's time to start building out your app

By now you should have been able to see how a BetterForms site can interact with your FileMaker solution. Where you go from here depends on the type of app you're building. Browse the [Common Customizations](https://github.com/DelfsEngineering/fm-betterforms/blob/master/getting-started/ide-quick-tour/4.-common-customizations-and-expanding-your-app) section for additional tips as you're getting started, in addition to the resources listed here on this page:

## Using the Examples

The BetterForms interface comes with a host of example pages that can be helpful to understand how certain elements work, or can be used as a great starting template for your own pages.

You can navigate to the [examples](https://app.fmbetterforms.com/#/examples) in the sidebar of the [BetterForms IDE](https://app.fmbetterforms.com/#/examples). The pages listed here will function just like your own pages except that you cannot edit them. If you want to use an example in your own app, simply duplicate it from the editor page and it will appear among your forms.

## Using Snippets

The most common JSON objects that you will need are stored in the snippets section to the left of the **Schema Editor**. Click the button on the snippet to copy it to your clipboard and paste it in

## Using the Community

BetterForms has a great Slack group where you can ask questions, find answers, and connect with other developers using BetterForms. If you're not already a member, request an invite after logging into the dashboard of the [BetterForms IDE](https://app.fmbetterforms.com/#/).

## Using the Forum

The Klai Studio forum holds many answers to common questions. [Create an account](https://forum.fmbetterforms.com/) for free and help contribute.


# Support & Resources


# Getting Help


# Learning JSON

BetterForms is heavily dependent on JSON for nearly all of its functionality, so knowing how JSON works is crucial to understanding BetterForms. Luckily, if you're new to JSON it's easy to learn!

Here are some great articles from around the web that can help your understanding:

* **Learn JSON in 10 Minutes (YouTube) -** <https://youtu.be/iiADhChRriM>
* **Intro to JSON -FileMaker Hacks** - <https://filemakerhacks.com/2018/04/23/thinking-about-json-part-1/>
* **Using FileMaker's JSON functions** - <https://luminfire.com/2017/09/30/filemaker-16s-new-json-functions/>


# Site Settings

A site in BetterForms is the core of your web app. It contains many settings that apply to all pages of your app. A site is defined by its **domain name**, although you can have multiple domains that share the same site settings.


# Navigation

You can define the site navigation object in site settings to build menu sections, links, action items, dropdowns, and custom HTML items.

## Navigation Shape

The navigation is defined in the **Appearance > Navigation** tab of your [site settings](/reference/site-settings).

It should be an array of section objects. Most apps use one or more sections, each with a `subs` array of menu items.

### Section Keys

| Key                   | Type                         | Description                                         |
| --------------------- | ---------------------------- | --------------------------------------------------- |
| `sectionLabel`        | `string`                     | Optional heading shown above the section            |
| `styleClassesSection` | `string`                     | CSS classes applied to the wrapping section `<nav>` |
| `styleClasses`        | `string`                     | CSS classes passed to the rendered item list        |
| `visible`             | `boolean` or function result | Controls whether the section renders                |
| `subs`                | `array`                      | Menu items for the section                          |

## Item Keys

Each object in `subs` can be one of several supported item types.

| Key            | Type                           | Description                                                |
| -------------- | ------------------------------ | ---------------------------------------------------------- |
| `label`        | `string`                       | Display label for links, action items, and dropdown labels |
| `icon`         | `string`                       | Optional icon classes                                      |
| `styleClasses` | `string`                       | CSS classes applied to the item                            |
| `visible`      | `boolean` or function result   | Controls whether the item renders                          |
| `path`         | `string`                       | Internal BetterForms route                                 |
| `exact`        | `boolean`                      | Passed to the router link for exact active matching        |
| `actions`      | `array` or named-action object | Action payload to run when the item is clicked             |
| `subs`         | `array`                        | Nested submenu items                                       |
| `html`         | `string`                       | Raw HTML item content                                      |

## How Items Resolve

The runtime supports these common patterns:

* If an item has `actions`, clicking it emits either:
  * `processActions` when `actions` is an action array
  * `processNamedAction` when the value is a named-action object with a `name`
* If an item has `path`, BetterForms renders it as a router link.
* If an item has `subs`, BetterForms renders it as a dropdown container for nested items.
* If an item has `html`, BetterForms renders that HTML directly.

Submenus automatically open when the current route matches a descendant item's `path`.

## Examples

```json
[
  {
    "sectionLabel": "Menu",
    "subs": [
      {
        "label": "Home",
        "icon": "fa fa-home",
        "path": "/dash"
      },
      {
        "label": "Some Form",
        "icon": "fa fa-check-square-o",
        "path": "/form/123-12-312-3123"
      },
      {
        "label": "A Dropdown Menu",
        "icon": "fa fa-car",
        "subs": [
          {
            "icon": "fa fa-car",
            "label": "Some Form",
            "path": "/dash"
          },
          {
            "icon": "fa fa-fw fa-play",
            "label": "Show Alert",
            "actions": [
              {
                "action": "showAlert",
                "options": {
                  "title": "Hello World!",
                  "text": "This item runs an action instead of routing directly.",
                  "type": "success"
                }
              }
            ]
          },
          {
            "icon": "fa fa-fw fa-list-ol",
            "label": "Show Modal",
            "actions": [
              {
                "action": "showModal",
                "options": {
                  "body": "This modal is followed by a route change.",
                  "icon": "success",
                  "options": {},
                  "overlayTheme": "dark",
                  "text": "This is a Modal"
                }
              },
              {
                "action": "path",
                "options": {
                  "path": "/dash"
                }
              }
            ]
          }
        ]
      }
    ]
  }
]
```

## Notes

* Navigation visibility is evaluated from the site/app context, not a page-specific form model.
* If you just need to route somewhere, prefer `path`.
* If you need a menu item to run a workflow first, use `actions` and include a `path` action in that workflow when needed.
* For route/action behavior details, see [path](/reference/actions-processor/actions_overview/path) and [Named Actions](/reference/actions-processor/actions_named).


# App Model

The App Model functions similar to a page's data model, except that it is accessible to every page in your site. You can also reference data in the App Model from elements that are not bound to a page

The **App Model** functions similar to a page's [data model](/reference/form-settings/data-model), except that it is accessible to every page in your site. You can also reference data in the App Model from elements that are not bound to a page, such as [Navigation menus](/reference/site-settings/navigationoverview#custom-navigation-menus) and [slots](/reference/site-settings/slots-code-injection). App data can be cached in local storage for fast performance.

### Referencing the App Model

The App model is simply a JSON object that can be globally referenced as `app.key`

* In a JavaScript function or calculation
  * `app.key`
* In VueJS template syntax (within HTML code)
  * `{{app.key}}`

### Inserting Data to the App Model

This can be done in several ways:

* Set the default data / keys in the **Environment > App Model** tab in your [**site settings**](/reference/site-settings).
* With a [**function action**](/reference/actions-processor/actions_overview/function-1), use the code `app.key = 'value'` to change or set data
* In a **Hook Script** set/modify the [$$BF\_App ](/reference/hooksoverview/filemaker-globals/usdusdbf_app)global variable using FileMaker's JSON functions

### Sync with Page Model Keys

In the page **Models** tab, you can enable **Sync with app** per key in **Key Caching**.\
When enabled, the page model key and app model key of the same name stay synchronized.

If a synced key is returned in both `$$BF_App` and `$$BF_Model` in a single hook response, the `$$BF_App` value is used as the final synced value.

### App Model Caching (bf-v0.10.4+)

The `app` data model keys can be individually cached in the browsers local or session storage.

This feature is enabled within the app model editing page of the page builder.

* **Use Local Storage** - This data will persist from session to session, and remain even after the browser has been closed.
* **Use Session Storage** - This data will survive a page refresh, but will not survive after the tab has been closed in the browser.

As your application loads, BetterForms will first check the **Session Storage** and if enabled, try to pull data from there, if that is not successful, it will check the **Local Storage** if enabled and attempt to populate data from there. By allowing separate control, you can have a given tabs context preserved.

Keys that are set for caching will automatically be saved locally as they are changed. No additional cache management is required.

{% hint style="warning" %}
**Security:** It is important to be mindful of sensitive data. Browser side caching is not encrypted and as such can be viewed directly by users.

You can clear local storage explicitly upon logout via an action function with the below JS code. This is not a security mitigation, but may be considered under some circumstances.

```
window.localStorage.clear();
```

{% endhint %}


# DOM Header Insertions

This are is used to add third part code into your app.

### Purpose

Use the DOM (Document Object Model ) insertions to add HTML, Meta Data tags, CDN's and JavaScript in your app. Code can be added and edited from the **Environment > DOM Header Insertions** tab in your [site settings](/reference/site-settings). This feature is useful for installing custom fonts or a [favicon](https://github.com/DelfsEngineering/fm-betterforms/blob/master/guides/styling/favicon.md), or other third-party [libraries](/reference/components-overview/3rd-party-elements).

### Code Insertion Sections

**DOM Header Insertions - Load First**

Klai Studio will load this code into your app before the first element of the app is rendered. Use this section when you need to ensure code or links are available before the app attempts to draw the page.

If you are upgrading an older BetterForms V2 project and need to restore legacy Bootstrap 3 `glyphicon` support for custom HTML, see the cookbook note on backward compatibility:

* [Backward Compatibility for V2 to V3](/cookbook-backlog/backward-compatibility-v2-to-v3)

**DOM Header Insertions - Load Later**

Code in this area is loaded at an undetermined point after the main application begins to load. This is ideal for adding libraries, like a payment gateway, into your app without affecting its loading and startup performance. Only put code here that your application does not need immediately. While it usually loads early, there is a possibility that rendering will start before this code loads.

#### Optimizations

Moving code and links into the load later section will decrease your application's initial loading time, particularly noticeable during the first load of the app. Klai Studio internally caches most of your external assets, so they will not need to be fetched on app revisits.


# Global Named Actions

Global named actions can be defined in the **Environment > Named Actions** tab in your [site settings](/reference/site-settings). When a named action is called, BetterForms will first look to see if a page-specific named action is defined for that name and if not, then check for a site-wide named action.

Global named actions can be called from anywhere within the application. This makes them perfect for reuse. For the occasion when you need a special override to that global name action, you can use a locally scoped named action.

Typical Usage:

* Common actions that are located in header or footer slots
* Reused actions like `print` and `save`
* Reused navigation like `gotoPageListview`

For more details about named actions, see this page:

{% content-ref url="/pages/lrn8Yi0CzsjybKPGBlM3" %}
[Named Actions (Action Scripts)](/reference/actions-processor/actions_named)
{% endcontent-ref %}


# Site Structure

Klai Studio app's do have some small opinionated design elements. Understanding the undunderlying structure will help you to design better apps.

## App Sections

There are two main sections of your app. The **Site** section which is global to the whole app and the **Page** section which is scoped to a particular page.

### Site Structure

The site structure consists of main parts.

* **Header** - This is the top section of the app. The header contains several slots
* **Navigation** - This navigation consists of the leftmost sidebar
* **Page** - This is the main working area you will use to build your app's page content. Note the page section technically extends up behind the

<figure><img src="/files/HgAHGJQY7EMepNMsdIxV" alt=""><figcaption><p>Digram of page structure</p></figcaption></figure>


# Slots / Code Injection

Reference for BetterForms slot names and where each slot renders in the app UI.

Slots are reusable HTML or Vue template insertion points that let you customize the BetterForms app shell and page wrapper without editing the core application code.

To edit slots in the IDE, go to:

* `Styling > Slots`

## Reference

| Slot Name             | Where it renders                                                       | Notes                                                                       |
| --------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `header`              | Standalone app-level header area rendered below the top navigation bar | App-shell level slot handled separately from the named header-bar slots     |
| `headerBrandLeft`     | Left of the brand/logo area in the top navigation bar                  | Header-bar slot                                                             |
| `headerBrand`         | Brand / app-name area in the top navigation bar                        | Replaces the default app-name block                                         |
| `headerSidebarToggle` | Sidebar toggle area in the top navigation bar                          | Replaces the default left-nav toggle button                                 |
| `headerLeft`          | Left-side header content area                                          | Header-bar slot                                                             |
| `headerCenter`        | Center header content area                                             | Current runtime slot name; use this instead of older `headerMiddle` wording |
| `headerRight`         | Right-side header content area                                         | Header-bar slot                                                             |
| `logout`              | Logout area in the top navigation bar                                  | Only shown when the user is authenticated                                   |
| `sidebarLeftTop`      | Above the left navigation menu items                                   | Sidebar slot                                                                |
| `sidebarLeftBottom`   | Below the left navigation menu items                                   | Sidebar slot                                                                |
| `sidebarLeftFooter`   | Footer area of the left navigation bar                                 | Often used for logos or custom footer content                               |
| `formHeader`          | Above the current page body                                            | Page-wrapper slot inside the form layout                                    |
| `formFooter`          | Below the current page body                                            | Page-wrapper slot inside the form layout                                    |
| `appFooter`           | Footer area of the overall app shell                                   | App-shell level footer slot                                                 |

## Notes

* Slots can replace or extend default shell content depending on where they are inserted.
* `logout` is a conditional slot because the default logout control only renders for authenticated users.
* `formHeader` and `formFooter` are page-wrapper slots, so they render around the current page content rather than inside the top navigation or sidebar.
* `appFooter` renders at the app-shell level and is separate from page-level footer content.

## Advanced Note

Some slot templates can access runtime browser-side objects such as `window.formGen`, depending on where the slot is rendered and what page context is currently active.

Treat that as advanced behavior:

* it is useful for custom dynamic templates
* it should not be the main mental model for learning slots
* if you rely on it heavily, document the expected page context clearly in your own implementation notes


# Page Settings

When loading the page editor, you'll be presented with various configuration options.

To learn more about building the elements of your page using the **Schema Editor**:

{% content-ref url="/pages/XQbydV3ndJENQr9kluCC" %}
[Page Elements](/reference/components-overview)
{% endcontent-ref %}


# Data Model

The **Data Model** tab contains all data that can be seen by the current page in JSON format.

## Default vs Development Data

The **Production data** field is the JSON object that loads into the page model when the page first renders. This is the baseline model used for normal runtime behavior and server hooks such as `onFormRequest`.

**Development data** is IDE-only. BetterForms merges it into the production data while previewing or working in editor tools such as the HTML editor.

This lets you work with realistic sample data in the editor without changing the page's real default runtime model.

## What The Page Model Is Used For

The page model is the browser-side state for the current page.

It is commonly used for:

* field binding through `model` paths
* HTML / Vue template rendering
* page-level named actions and client workflows
* scoped and utility hook payloads

## Utility Hook Payloads

When `runUtilityHook` sends data to FileMaker, BetterForms can reduce or reshape the outgoing payload.

See:

{% content-ref url="/pages/OPunE2gEJir0LExcRMN2" %}
[Reducing Payload Size](/reference/hooksoverview/env_vars)
{% endcontent-ref %}

That page covers:

* reduced payload mode when full schema sending is off
* `modelFilterKeys`
* `options.model`
* merge vs replace behavior for returned `result.model`

## Key Caching

Page-model keys can be cached individually through `modelCaches`.

Each cache entry is configured per model key path and can enable:

* `localStorage`
* `sessionStorage`
* `appSync`

At load time, BetterForms checks session storage first, then local storage for configured keys, and applies any stored values back into the page model.

As values change, BetterForms writes them back to the configured browser storage automatically.

## Sync With App

If a model-cache entry enables `appSync`, BetterForms keeps that page-model key aligned with the app model key of the same path.

This is useful when a value should behave like page state in some places but still remain available app-wide.

For broader app-level behavior, see:

* [App Model](/reference/site-settings/app-model)

## Merge Behavior After Utility Hooks

When FileMaker returns `result.model` from a utility hook, BetterForms now defaults to **merge** behavior unless the response explicitly sets `state.modelUpdateMode = "replace"`.

That means:

* returned keys update the current page model
* client-only reactive keys can be preserved
* explicit `null`, empty strings, or empty arrays from FileMaker still clear values when returned

If a key is configured with `appSync` and the same key is returned in both the page model and app model, the app-model value wins for the final synced value.


# Card / Window Modals

Card modals display other pages as their content.

{% hint style="info" %}
This element is based on the [**vue js modal**](https://github.com/euvl/vue-js-modal#properties) module.
{% endhint %}

Card Modals are a special type of modal that show the contents of another page as their contents.

They are opened with an `showCardModal` action and hidden with a `hideCardModal` action.

```yaml
// Minimal settings
{
    "action": "showCardModal",
    "options": {
        "slug": "some_page_slug" // note: slug names must be URL comaptible
    }
}
```

## Options

The following items can exist in the options object for the `showCardModal` action. Either `slug` or `idForm` must be present to identify the page loaded in the card modal. See [source docs](https://euvl.github.io/vue-js-modal/Properties.html) for a complete list of additional parameters.

<table><thead><tr><th width="300.3333333333333" align="right">Option Key</th><th width="110" align="center">Type</th><th>Description / Purpose</th></tr></thead><tbody><tr><td align="right"><code>name</code></td><td align="center">string</td><td>Add a name to the card window. If not set, it will default to "modal". You can use the modal to target the window for other actions.</td></tr><tr><td align="right"><code>clickToClose</code></td><td align="center">boolean</td><td>defaults true. If false, you will not be able to click outside of the modal to close it.</td></tr><tr><td align="right"><code>draggable</code></td><td align="center">boolean</td><td>defines a CSS selector for an element that should be allowed to drag the entire model if clicked on by the user</td></tr><tr><td align="right"><code>height</code></td><td align="center">string</td><td>pixel or % value to define the height of the modal</td></tr><tr><td align="right"><code>idForm</code></td><td align="center">string</td><td>(optional - instead of slug) UUID of the page to show in the card</td></tr><tr><td align="right"><code>model</code></td><td align="center">object</td><td>if set, this data will be used for the page data model. If an <code>onFormRequest</code> hook is enabled, this content will NOT be passed into the hook.</td></tr><tr><td align="right"><code>onBeforeClosed_actions</code></td><td align="center">array</td><td>Array of actions to be run when the card window is closed. This and the `onClosed` is useful if you want to run actions when a user clicks outside the modal without explicitly clicking a button.</td></tr><tr><td align="right"><code>onClosed_actions</code></td><td align="center">array</td><td>Array of actions run <em>after</em> the modal has been closed.</td></tr><tr><td align="right"><code>query</code></td><td align="center">object</td><td>Data passed to FMS as if it were a query param (more below)</td></tr><tr><td align="right"><code>resizable</code></td><td align="center">boolean</td><td>defaults false. If true, the model can be resized by the user</td></tr><tr><td align="right"><code>scrollable</code></td><td align="center">boolean</td><td>defaults true. If false, the modal's content will be fixed in view (user cannot scroll down the page)</td></tr><tr><td align="right"><code>slug</code></td><td align="center">string</td><td>Must match a site navigation slug of the page to show in the card</td></tr><tr><td align="right"><code>classes</code></td><td align="center">string</td><td>Classes that will be applied to to modal window wrapper</td></tr><tr><td align="right"><code>width</code></td><td align="center">string</td><td>pixel or % value to define the width of the modal</td></tr></tbody></table>

Changing paths / page contents, use a `path` action with added `windowName` key to target the correct window, in this case the name of this modal.

## Passing Parameters

Often you need to pass some params to the modal from the parent page. These fall within situations, with or without the `onFormRequest` hook enabled.

#### With `onFormRequest` enabled

When called from another (parent) page. If you need `onFormRequest` params, use the `query` key as you would with other pages and read via the `$$BF_Query` global in your hook script.

#### Without `onFormRequest` enabled

When showing a modal and you don't need to call a server hook script you can still pass data to modal several ways.

1. Via the `model` key. data in the key will appear in the `model` for the modal. This is useful when you want to edit some detail information that the parent form already has.
2. Via the `app` object. By setting a key within the `app` object, and then later reading that when the form loads, you can pass anything back and forth between parent and modal.


# Validation

Client side validation is controlled by the `validator` key in all page elements.

```yaml
// Example
{
  "label": "Last Name",
  "model": "nameLast",
  "required": true,
  "styleClasses": "col-md-3",
  "type": "input",
  "fieldOptions": {
    "inputType": "text"
  },
  "validator": "string"
}
```

Validation **does not** run automatically. You must trigger client validation with the [validate action.](/reference/actions-processor/actions_overview/validate)

### **The validator key can be set to the following:**

**number** Checks that the value is numeric - and that it's within the fields min & max range, if these are defined in the schema. **integer** Checks that the value is a valid Javascript Number - and that it's an integer.

**double** Checks that the value is a valid number.

{% hint style="warning" %}
When validating with **number**, **integer**, or **double**, make sure `fieldOptions.inputType` is set to `number`, otherwise the field will be treated as a text-style input in the browser.
{% endhint %}

**string** Checks that the value is a string - and that its length is within the fields min & max range, if these are defined in the schema.

**array** Checks that the value is an array - and that the arrays length is within the fields min & max range, if these are defined in the schema. Expects the value to be a valid Javascript array literal - something like this: \["John", "Doe", "Jane"] or \[1, 2, 3].

**date** Checks that the value is a valid Javascript Date - and that the date is between the fields min and max dates, if these are defined in the schema.

**regexp** Checks that the value matches the regex defined in the fields schema.pattern. If schema.pattern isn't set, validation is skipped.

**email** Checks that the value is a plausible looking email address, using a regular expression.

**url** Checks that the value is a plausible looking http url, using a regular expression. Among other checks, the URL needs to start with http\:// or https\://.

**creditCard** Checks that the value is a valid credit card number, using code from here.

**alpha** Checks that the value is a letter, using this regex: /^\[a-zA-Z]\*$/

**alphaNumeric** Checks that the value is a letter or a number, using this regex: /^\[a-zA-Z0-9]\*$/

{% hint style="danger" %}
This validation works great as a user interface element, but should not be trusted in FileMaker. **Anything that comes from the a client's web browser can be hacked**, so sensitive data should also be validated in your FileMaker scripts before committing to the database.
{% endhint %}

Source: <https://icebob.gitbooks.io/vueformgenerator/content/validation/built-in-validators.html>


# Custom Validators

You can define your own Javascript custom validation calculation that will fire the same as the regular validators.

By selecting the `validator` type to `calc` and adding a calculation and error message you can have excellent control over validation.

!["Custom Validator"](/files/srmemDBQvPaDNajW7iZS)

```yaml
// example JSON for an element with custom validation
[
  {
    "label": "Some Password",
    "model": "password1",
    "placeholder": "",
    "styleClasses": "col-md-3",
    "type": "input",
    "fieldOptions": {
      "inputType": "text"
    },
    "required": true,
    "validator": "string"
  },
  {
    "label": "Another Password",
    "model": "password2",
    "placeholder": "",
    "styleClasses": "col-md-3",
    "type": "input",
    "fieldOptions": {
      "inputType": "text"
    },
    "required": true,
    "validator": "calc",
    "validator_calc": "model.password1 == model.password2",
    "errorMsg": "The passwords do not match"
  }
]
```


# SEO Meta Tags

> **⚠️ PRELIMINARY FEATURE - BETA**
>
> This feature is currently in beta testing. The API and functionality may change in future releases. We welcome feedback and suggestions as we refine this feature.

***

## Overview

The `seoMeta` field type allows you to define SEO meta tags at the form/page level. These tags are extracted during Server-Side Rendering (SSR) and injected into the HTML `<head>` section, enabling:

* **Search Engine Optimization** - Better indexing by Google, Bing, etc.
* **Social Media Previews** - Rich previews when links are shared on Slack, Facebook, LinkedIn, Twitter, Discord, etc.
* **Dynamic Meta Tags** - Use `_calc` expressions to populate tags from database data

***

## Field Structure

### Basic Schema

```json
{
  "type": "seoMeta",
  "metaTags": {
    "title": "Page Title - Your Brand",
    "description": "Page description for search results",
    "og:title": "Social Media Title",
    "og:image": "https://yourdomain.com/image.jpg"
  }
}
```

### Properties

| Property   | Type   | Required | Description          |
| ---------- | ------ | -------- | -------------------- |
| `type`     | String | Yes      | Must be `"seoMeta"`  |
| `metaTags` | Object | Yes      | Meta tag definitions |

***

## Behavior

### In Editor (GUI)

* **Visible** - Field appears in the form designer in the tree view
* **Editable** - Designers can modify meta tags
* **No Rendering** - Does not render in the actual form

### In Production (Runtime)

* **Hidden** - Field does not appear to users
* **SSR** - Meta tags are extracted during SSR for bots/crawlers
* **Client-Side** - Meta tags are also injected into `document.head` at runtime for regular users, enabling correct browser tab titles, social sharing from the SPA, and `<html lang>` attributes
* **Merged** - If multiple `seoMeta` fields exist on the page, their `metaTags` are merged and later fields override earlier ones

***

## Meta Tags Object

The `metaTags` object supports two types of values:

### 1. Static Values

```json
{
  "title": "About Us - Acme Corp",
  "og:image": "https://cdn.acme.com/about.jpg"
}
```

### 2. Dynamic Values with `_calc`

```json
{
  "title_calc": "model.product.name + ' - $' + model.product.price",
  "description_calc": "model.product.description || 'Default description'"
}
```

**Important:** The `_calc` suffix is removed when rendering:

* `title_calc` → becomes `title` in HTML
* `og:image_calc` → becomes `og:image` in HTML

***

## Supported Meta Tag Types

### Special Keys

These keys have unique behavior and do **not** render as `<meta>` tags:

| Key         | Behavior                                                                      |
| ----------- | ----------------------------------------------------------------------------- |
| `title`     | Sets `<title>` tag (browser tab / search result title)                        |
| `language`  | Sets `<html lang="...">` attribute. During SSR, defaults to `"en"` if omitted |
| `_comment*` | Ignored — for developer notes and organization                                |

### Standard Meta Tags

Use `name=""` attribute:

```json
{
  "description": "Page description",
  "keywords": "keyword1, keyword2",
  "author": "Your Name",
  "robots": "index, follow"
}
```

**Renders as:**

```html
<meta name="description" content="Page description">
<meta name="keywords" content="keyword1, keyword2">
```

***

### Open Graph (Facebook, LinkedIn, Slack)

Use `property=""` attribute:

```json
{
  "og:title": "Social Media Title",
  "og:description": "Social media description",
  "og:image": "https://example.com/image.jpg",
  "og:type": "website",
  "og:url": "https://example.com/page"
}
```

**Renders as:**

```html
<meta property="og:title" content="Social Media Title">
<meta property="og:image" content="https://example.com/image.jpg">
```

***

### Twitter Card

Use `name=""` attribute:

```json
{
  "twitter:card": "summary_large_image",
  "twitter:title": "Twitter Title",
  "twitter:description": "Twitter description",
  "twitter:image": "https://example.com/twitter.jpg"
}
```

**Renders as:**

```html
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Twitter Title">
```

***

### Article Meta (Blog Posts)

Use `property=""` attribute:

```json
{
  "og:type": "article",
  "article:published_time": "2024-01-15T10:00:00Z",
  "article:author": "John Doe",
  "article:section": "Technology"
}
```

***

### Product Meta (E-commerce)

Use `property=""` attribute:

```json
{
  "og:type": "product",
  "product:price:amount": "29.99",
  "product:price:currency": "USD",
  "product:availability": "in stock"
}
```

***

## Dynamic Expressions with `_calc`

Add `_calc` to any key to evaluate it as a JavaScript expression. The expression has access to the `model` object from the form's data model.

```json
{
  "title_calc": "model.product.name + ' | My Store'"
}
```

If both a static key and its `_calc` counterpart exist, `_calc` takes precedence.

***

## Complete Examples

### Static Page

```json
{
  "type": "seoMeta",
  "metaTags": {
    "title": "About Us - Acme Corporation",
    "description": "Learn about Acme Corp's mission, values, and team",
    "og:title": "About Acme Corp",
    "og:image": "https://cdn.acme.com/about-og.jpg",
    "og:type": "website",
    "twitter:card": "summary_large_image"
  }
}
```

### Dynamic Page (using `_calc`)

Any key with a `_calc` suffix is evaluated as a JavaScript expression with access to `model`. The `_calc` version takes priority over the static key.

```json
{
  "type": "seoMeta",
  "metaTags": {
    "title_calc": "model.product.name + ' - $' + model.product.price + ' | Acme Store'",
    "description_calc": "model.product.description?.substring(0, 160)",
    "og:title_calc": "model.product.name",
    "og:image_calc": "model.product.images?.[0]?.url || 'https://cdn.acme.com/default.jpg'",
    "og:type": "product",
    "twitter:card": "summary_large_image"
  }
}
```

***

## Requirements

### Server-Side Rendering (SSR) for crawlers and social previews

The `seoMeta` field also works in the browser at runtime, but SSR must be enabled if you want bots and social crawlers to receive the tags in the initial HTML response. Meta tags are extracted and rendered server-side when:

1. A bot/crawler accesses the page (Googlebot, Facebookbot, etc.)
2. SSR is enabled for the form's layout in site settings
3. The form is successfully loaded with data

### Form Data (`onFormRequest`)

For **dynamic** `_calc` expressions:

* The form's `onFormRequest` hook must be enabled
* The hook must populate the `model` object with data
* SSR will fetch this data automatically

For **static** values:

* No `onFormRequest` needed
* Tags are rendered as-is

***

## Validation & Best Practices

### Character Limits

| Tag              | Recommended Length |
| ---------------- | ------------------ |
| `title`          | 50-60 characters   |
| `description`    | 150-160 characters |
| `og:title`       | 60-90 characters   |
| `og:description` | 55-65 characters   |
| `twitter:title`  | 70 characters      |

### Image Requirements

| Platform                               | Size            | Aspect Ratio |
| -------------------------------------- | --------------- | ------------ |
| Open Graph (Facebook, LinkedIn, Slack) | 1200x630px      | 1.91:1       |
| Twitter Large Image                    | 1200x675px      | 16:9         |
| Twitter Summary                        | 120x120px (min) | 1:1          |

### Best Practices

1. ✅ **Always include fallbacks** in `_calc` expressions
2. ✅ **Use absolute URLs** for images
3. ✅ **Test previews** on actual platforms (Slack, Facebook, etc.)
4. ✅ **Keep descriptions concise** and compelling
5. ✅ **Use high-quality images** (1200x630px minimum)
6. ❌ **Don't use relative URLs** for og:image
7. ❌ **Don't exceed character limits** (truncated by platforms)

***

## Testing

### Test as Bot

Use curl with a bot user-agent:

```bash
curl -H "User-Agent: Googlebot" https://yourdomain.com/page
```

Look for meta tags in the `<head>` section.

### Test Social Previews

* **Facebook Debugger:** <https://developers.facebook.com/tools/debug/>
* **Twitter Card Validator:** <https://cards-dev.twitter.com/validator>
* **LinkedIn Inspector:** <https://www.linkedin.com/post-inspector/>
* **Slack:** Just paste the link in any channel

***

## Troubleshooting

### Meta Tags Not Appearing

**Check:**

1. Is SSR enabled for this layout?
2. Is the form loading successfully?
3. Are you testing with a bot user-agent?
4. Does the `seoMeta` field exist in the form schema?

### Dynamic Values Not Working

**Check:**

1. Is `onFormRequest` enabled for the form?
2. Is the `model` object populated with data?
3. Are `_calc` expressions syntactically correct?
4. Check server logs for evaluation errors

### Social Previews Not Updating

**Solution:**

* Clear the platform's cache using their debugging tools
* Facebook/LinkedIn/Twitter cache previews for 7 days
* Use their inspector tools to force a refresh

***

## Security

### Escaping

Server-rendered meta tag values are HTML-escaped before they are written into the SSR response:

```javascript
<script>alert('xss')</script>
// Becomes:
&lt;script&gt;alert('xss')&lt;/script&gt;
```

### `_calc` Expressions

`_calc` expressions are evaluated with access to the page `model`, and evaluation failures are caught and logged. Keep these expressions simple and side-effect free.

***

## Auto-Generated Tags

The following are injected automatically if not explicitly defined in `metaTags`:

| Tag                      | Value            |
| ------------------------ | ---------------- |
| `<link rel="canonical">` | Current page URL |
| `og:url`                 | Current page URL |

***

## Page Language

Set the page language with the `language` key:

```json
{
  "language": "fr",
  "title": "Titre de la Page"
}
```

**Renders as:**

```html
<html lang="fr">
```

For dynamic language based on model data:

```json
{
  "language_calc": "model.userLanguage || 'en'"
}
```

If omitted, SSR falls back to `"en"`.

***

## Dynamic robots.txt and sitemap.xml

BetterForms automatically generates `robots.txt` and `sitemap.xml` for each tenant.

Static pages with `ssr.enabled: true` are included automatically.

For dynamic clean URLs such as product detail pages or blog posts, BetterForms can also expand the sitemap through the common `onSiteMap` hook. This is important because search engines discover and index the clean server-visible URL, not the browser hash URL.

Example `onSiteMap` response:

```json
{
  "response": {
    "urls": [
      { "loc": "/products/PRD_D3758281-4BE9-4DAC-9E74-3ED4DAE718C2" },
      { "loc": "/products/PRD_7D0CD18C-876A-4CBD-B157-C8E638FD4E7D", "lastmod": "2026-04-11" }
    ]
  }
}
```

Rules:

* static SSR layouts are listed automatically
* dynamic URLs should be returned by `onSiteMap`
* `loc` must be a clean path beginning with `/`
* return clean URLs, not hash URLs such as `/#/products/...`
* `lastmod` is optional

If `onSiteMap` is not implemented, BetterForms still returns a valid static-only sitemap.

***

## Related Documentation

* [Form Settings Overview](/reference/form-settings)
* [Data Model](/reference/form-settings/data-model)
* [SEO and Social Sharing Guide](/reference/styling/seo-and-social-sharing)
* [DOM Header Insertions](/reference/site-settings/dom-header-insertions)

## Feedback

This is a preliminary feature. Please share feedback or suggestions in the Slack channel **#suggestions**.

***

*Last Updated: February 2026*


# Misc Page Settings

This page covers the page-level form keys that affect layout, shell behavior, routing, and wizard behavior.

## Core Page Keys

The runtime reads these keys from `form`.

| Key                | Purpose                                                                        |
| ------------------ | ------------------------------------------------------------------------------ |
| `formType`         | Selects which page renderer BetterForms mounts                                 |
| `styleClassesPage` | CSS classes applied to the outer page wrapper                                  |
| `styleClassesBody` | CSS classes applied to the page body / form body                               |
| `hideHeader`       | Hides the page header block that normally renders `form.title` and `form.text` |
| `isForm`           | Renders the page body as an actual HTML `<form>` instead of a `<div>`          |
| `requestHook`      | Affects form-fetch behavior and disables the local cached-form shortcut        |
| `authLevel`        | Affects whether an authenticated form can be served from cache while online    |

## `formType`

The current runtime supports these page renderers:

* `formblank`
* `formwidget`
* `formwizard`
* `formplain`

`formblank` is the default single-page renderer.

`formwizard` mounts the wizard layout and enables wizard-specific keys such as button labels, transitions, and tab validation behavior.

`formplain` is still present in the runtime as a legacy renderer.

## `isForm`

If `form.isForm` is true, BetterForms renders the page body as a real HTML `<form>`.

This is most useful for flows like [Custom Login Pages](/reference/authentication/custom-login-pages), where pressing **Enter** should submit the page-level form naturally.

In practice, pair `isForm` with a [button](/reference/components-overview/common/button) that is configured to submit in the same form context.

## Header And Page Styling

* `styleClassesPage` controls classes on the outer page wrapper.
* `styleClassesBody` controls classes on the page body area used by the mounted form renderer.
* `hideHeader` removes the default page header block that would otherwise show the page title/text for non-blank form types.

## Fetch / Cache Related Keys

Two page keys influence how BetterForms fetches and reuses page definitions:

* `requestHook`: if true, BetterForms does not reuse the cached form definition shortcut for that page
* `authLevel`: when `authLevel === 1`, BetterForms requires authentication before reusing that cached page while online

These keys matter most for pages that rely on dynamic server-side page setup or authentication-gated access.

## Wizard Keys

When `formType` is `formwizard`, the runtime reads these additional keys from `form`:

| Key                           | Purpose                                                    |
| ----------------------------- | ---------------------------------------------------------- |
| `title` / `subtitle`          | Standard wizard header text passed to the wizard component |
| `wizardTitle`                 | Custom slot title rendered above the tabs                  |
| `color` / `errorColor`        | Wizard accent and error colors                             |
| `shape`                       | Wizard step shape                                          |
| `stepSize`                    | Wizard step sizing                                         |
| `validateOnBack`              | Controls validation when moving backward                   |
| `nextButtonText`              | Next button label                                          |
| `backButtonText`              | Back button label                                          |
| `finishButtonText`            | Finish button label                                        |
| `transition`                  | Transition name passed to the wizard                       |
| `wizardEnableAllTabs`         | Marks all tabs as available on mount                       |
| `wizardAllowInvalidTabChange` | Allows tab changes even when the current tab is invalid    |
| `state.startIndex`            | Starting tab index when loading the wizard                 |

## Related Pages

* [Form Types](https://github.com/DelfsEngineering/fm-betterforms/blob/master/reference/form-types.md)
* [Custom Login Pages](/reference/authentication/custom-login-pages)
* [Button](/reference/components-overview/common/button)


# Page Elements

Pages in BetterForms support a wide variety of element types to build rich, interactive user interfaces. Many of the foundational input fields and form controls are based on a robust and widely-used schema-based form generation library. You can find documentation for these [BetterForms Elements](/reference/components-overview/betterforms-elements) in their dedicated section, which includes links to comprehensive external references for all their detailed properties and advanced usage.

Beyond these standard elements, the BetterForms framework introduces numerous enhancements, makes specific adaptations, and includes many custom-built components that are unique to BetterForms or integrate specialized functionalities. These are detailed in the subsequent sections.

This documentation aims to cover how all elements are used within BetterForms, highlighting any deviations from or additions to the base functionalities of standard elements, and providing full details for BetterForms-specific components.

To learn more about how pages are built, see the Getting started guide:

{% content-ref url="/pages/zuYpxivAUUNClmbRy2NB" %}
[2.4 Adding Elements to Your Page](/getting-started/ide-quick-tour/first-app/adding-elements)
{% endcontent-ref %}


# App Layout Overview

Quick pointer to the canonical BetterForms app layout overview.

This page exists as a pointer for readers browsing the page-elements section.

For the canonical explanation of the BetterForms app shell layout, use:

* [Site Structure](/reference/site-settings/site-structure)

That page covers:

* the main app sections
* where the header, navigation, and page body render
* the published layout diagram


# BetterForms Elements

This section covers the field types and input-oriented elements you can place on BetterForms pages.

In practice, this section mixes three kinds of docs:

* BetterForms-owned fields and wrappers
* Vue Form Generator-backed fields that BetterForms still uses at runtime
* Compatibility-oriented pages where BetterForms normalizes older field shapes into the current V3+ `fieldOptions` shape

When a field page shows `fieldOptions`, treat that as the canonical V3+ contract unless the page clearly says otherwise.

These pages focus on the BetterForms-facing schema and behavior you need in the editor. Where a field ultimately comes from a dependency, the page should still describe the runtime shape BetterForms expects.

Browse the sub-pages for the specific field you need.


# Checkbox

The Checkbox element presents an option that can be toggled on (checked) or off (unchecked), typically representing a boolean choice (true/false).

In BetterForms, this is commonly used for yes/no questions, agreement to terms, or selecting individual binary options.

## Common Configuration Properties

Below are some of the most common properties used when configuring a Checkbox element in BetterForms:

| Property       | Type               | Description                                                                                                                               |
| -------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `type`         | `String`           | Must be set to `"checkbox"`.                                                                                                              |
| `label`        | `String`           | The text displayed next to the checkbox to describe its purpose.                                                                          |
| `model`        | `String`           | The key in your BetterForms data model where the checkbox's boolean value (true/false) will be stored.                                    |
| `disabled`     | `Boolean`          | If set to `true`, the checkbox will be visible but not interactive. Defaults to `false`.                                                  |
| `default`      | `Boolean`          | The initial state of the checkbox (e.g., `true` for checked, `false` for unchecked).                                                      |
| `hint`         | `String`           | Additional helper text displayed with the checkbox, often as a tooltip or small text below.                                               |
| `required`     | `Boolean`          | If set to `true`, the form will require this checkbox to be checked for submission (less common for checkboxes unless it's an agreement). |
| `featured`     | `Boolean`          | Can be used by themes to apply special styling, often making the element more prominent.                                                  |
| `styleClasses` | `String` / `Array` | CSS class(es) to apply directly to the checkbox wrapper for custom styling.                                                               |

### Example Schema Snippet

```json
{
  "type": "checkbox",
  "label": "I agree to the terms and conditions",
  "model": "agreedToTerms",
  "default": false,
  "required": true,
  "hint": "You must agree to proceed."
}
```

## BetterForms Specific Notes

* The `model` property is crucial and must map to a field in your BetterForms data model designed to store a boolean value.
* For scenarios requiring a user to select multiple items from a list of checkboxes, consider using the `checklist` element instead.

## Full Property Reference

This element is based on a standard form generation library. For a comprehensive list of all available properties, advanced configuration options, and the full technical specification, please refer to the [Checkbox Field Documentation](https://vue-generators.gitbook.io/vue-generators/fields/core-fields/checkbox).


# Checklist

The Checklist element allows users to select multiple options from a predefined list. Each option is typically represented by a checkbox.

In BetterForms, this is ideal for scenarios where a user can choose one or more items from a set of available choices, such as selecting multiple interests, toppings, or features.

## Common Configuration Properties

Below are some of the most common properties used when configuring a Checklist element in BetterForms:

| Property       | Type               | Description                                                                                                                                                      |
| -------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`         | `String`           | Must be set to `"checklist"`.                                                                                                                                    |
| `label`        | `String`           | A general label for the entire checklist group.                                                                                                                  |
| `model`        | `String`           | The key in your BetterForms data model where the array of selected values will be stored.                                                                        |
| `values`       | `Array`            | An array of strings or objects defining the individual options in the checklist. If objects, they usually have `name` (display) and `value` (stored) properties. |
| `listBox`      | `Boolean`          | If `true`, displays the checklist in a style resembling a list box, often with a border. Defaults to `false`.                                                    |
| `disabled`     | `Boolean`          | If `true`, all checkboxes in the list will be visible but not interactive. Defaults to `false`.                                                                  |
| `hint`         | `String`           | Additional helper text displayed with the checklist group.                                                                                                       |
| `required`     | `Boolean`          | If `true`, the form will require at least one option to be selected. Defaults to `false`.                                                                        |
| `featured`     | `Boolean`          | Can be used by themes to apply special styling to the checklist group.                                                                                           |
| `styleClasses` | `String` / `Array` | CSS class(es) to apply to the checklist wrapper for custom styling.                                                                                              |

### Example Schema Snippet

```json
{
  "type": "checklist",
  "label": "Select your interests:",
  "model": "userInterests",
  "values": [
    { "name": "Sports", "value": "sports" },
    { "name": "Music", "value": "music" },
    { "name": "Technology", "value": "tech" },
    { "name": "Travel", "value": "travel" }
  ],
  "listBox": true,
  "hint": "Choose as many as you like."
}
```

## BetterForms Specific Notes

* The `model` property should map to a field in your BetterForms data model that can store an array of the selected values.
* The `values` array is fundamental for defining the options available to the user.

## Full Property Reference

This element is based on a standard form generation library. For a comprehensive list of all available properties, advanced configuration options, and the full technical specification, please refer to the [Checklist Field Documentation](https://vue-generators.gitbook.io/vue-generators/fields/core-fields/checklist).


# Cleave.js Input Masking

The Cleave.js element provides formatted input masks for various types of data, such as phone numbers, credit card numbers, dates, and custom patterns. It enhances user experience by guiding input format and providing immediate visual feedback.

In BetterForms, this element is used when you need to enforce a specific input format for a text field.

**Note:** This element uses `type: "cleave"` in its schema definition, and its masking behavior is controlled by `fieldOptions`.

## Common Configuration Properties

| Property       | Type               | Description                                                                                         |
| -------------- | ------------------ | --------------------------------------------------------------------------------------------------- |
| `type`         | `String`           | Must be set to `"cleave"`. The Cleave.js functionality is configured through `fieldOptions`.        |
| `label`        | `String`           | The label for the input field.                                                                      |
| `model`        | `String`           | The key in your BetterForms data model where the (potentially unmasked) input value will be stored. |
| `placeholder`  | `String`           | Placeholder text.                                                                                   |
| `disabled`     | `Boolean`          | Disables the input.                                                                                 |
| `readonly`     | `Boolean`          | Makes the input read-only.                                                                          |
| `hint`         | `String`           | Helper text.                                                                                        |
| `required`     | `Boolean`          | Marks the field as required.                                                                        |
| `styleClasses` | `String` / `Array` | CSS classes for styling.                                                                            |
| `fieldOptions` | `Object`           | **Crucial property.** Options passed directly into `new Cleave(...)`.                               |

### Example: Phone Number Mask

```json
{
  "type": "cleave",
  "label": "Phone Number",
  "model": "phoneNumber",
  "placeholder": "(999) 999-9999",
  "fieldOptions": {
    "phone": true,
    "phoneRegionCode": "US"
  }
}
```

### Example: Date Mask (YYYY-MM-DD)

```json
{
  "type": "cleave",
  "label": "Date of Birth",
  "model": "dob",
  "placeholder": "YYYY-MM-DD",
  "fieldOptions": {
    "date": true,
    "datePattern": ["Y", "m", "d"],
    "delimiter": "-"
  }
}
```

### Example: Credit Card

```json
{
  "type": "cleave",
  "label": "Credit Card Number",
  "model": "creditCardNum",
  "fieldOptions": {
    "creditCard": true
  }
}
```

## BetterForms Specific Notes

* The live field writes back `cleave.properties.result` when available, or falls back to the input's current value.
* If you omit settings, the component supplies defaults such as `phoneRegionCode: "AU"`, `datePattern: ["d", "m", "Y"]`, `delimiter: " "`, and `numeralDecimalScale: 2`.
* `fieldOptions` supports the standard Cleave.js configuration keys such as `creditCard`, `phone`, `date`, `numeral`, `blocks`, `delimiter`, `prefix`, `uppercase`, and `lowercase`.

## Full Property Reference

This element leverages the Cleave.js library. For the available `fieldOptions`, refer to:

* [VFG Cleave Field Documentation](https://vue-generators.gitbook.io/vue-generators/fields/optional-fields/cleave)
* [Official Cleave.js Documentation](https://nosir.github.io/cleave.js/)


# DateTime Picker

The live `dateTimePicker` field is a text input enhanced by the jQuery Bootstrap DateTimePicker plugin.

## Common Configuration Properties

| Property       | Type      | Description                                                    |
| -------------- | --------- | -------------------------------------------------------------- |
| `type`         | `String`  | Must be `"dateTimePicker"`                                     |
| `label`        | `String`  | Field label                                                    |
| `model`        | `String`  | Model key that stores the input value                          |
| `placeholder`  | `String`  | Placeholder text                                               |
| `disabled`     | `Boolean` | Disables the input                                             |
| `readonly`     | `Boolean` | Makes the input read-only                                      |
| `fieldOptions` | `Object`  | Options passed directly to the Bootstrap DateTimePicker plugin |

## `fieldOptions`

The component passes `fieldOptions` directly into `$(el).datetimepicker(...)`.

If you do not provide a `format`, the field uses the component's default input format from the shared date helper.

Example:

```json
{
  "type": "dateTimePicker",
  "label": "Appointment Time",
  "model": "appointmentDateTime",
  "placeholder": "Select date and time",
  "fieldOptions": {
    "format": "MM/DD/YYYY hh:mm A"
  }
}
```

## Runtime Notes

* The component requires the Bootstrap DateTimePicker library to be loaded globally.
* The field stores the input text value selected in the picker.
* A calendar icon addon is rendered on the right side of the input.

## Full Property Reference

This element is implemented in the sibling `vue-form-generator` source as `fieldDateTimePicker.vue` and uses the Bootstrap DateTimePicker jQuery plugin.


# Google Address Autocomplete

The live `googleAddress` field is a text input enhanced by `google.maps.places.Autocomplete`.

## Common Configuration Properties

| Property                      | Type       | Description                                          |
| ----------------------------- | ---------- | ---------------------------------------------------- |
| `type`                        | `String`   | Must be `"googleAddress"`                            |
| `label`                       | `String`   | Field label                                          |
| `model`                       | `String`   | Model key that stores the selected formatted address |
| `placeholder`                 | `String`   | Placeholder text                                     |
| `disabled`                    | `Boolean`  | Disables the input                                   |
| `readonly`                    | `Boolean`  | Makes the input read-only                            |
| `fieldOptions.onPlaceChanged` | `Function` | Optional callback fired after a place is selected    |

## Runtime Notes

* The component requires the Google Maps JavaScript API with the `places` library loaded globally.
* The model value is set to `place.formatted_address`.
* On focus, the component attempts to use browser geolocation to bias autocomplete results near the user.
* The autocomplete instance is created with `types: ["geocode"]`.

If `fieldOptions.onPlaceChanged` is provided, it is called with:

```
(formattedAddress, parsedAddressData, place, model, schema)
```

`parsedAddressData` may include keys such as:

* `street_number`
* `route`
* `country`
* `administrative_area_level_1`
* `administrative_area_level_2`
* `locality`
* `postal_code`

## Example Schema Snippet

```json
{
  "type": "googleAddress",
  "label": "Street Address",
  "model": "userAddress",
  "placeholder": "Start typing your address..."
}
```

## Full Property Reference

This element is implemented in the sibling `vue-form-generator` source as `fieldGoogleAddress.vue` and uses the Google Places Autocomplete API.


# Image Display Element

The live `image` field supports both displaying an image and selecting a local file that is then stored into the model as a data URL.

In BetterForms, this field is useful for lightweight image preview and simple inline image capture. For larger upload workflows, use the dedicated uploader components.

## Common Configuration Properties

| Property       | Type               | Description                                                                                 |
| -------------- | ------------------ | ------------------------------------------------------------------------------------------- |
| `type`         | `String`           | Must be set to `"image"`.                                                                   |
| `label`        | `String`           | An optional label for the image, which might be displayed as a caption or title.            |
| `model`        | `String`           | The key in your BetterForms data model that holds the image URL or Base64 data string.      |
| `styleClasses` | `String` / `Array` | CSS class(es) to apply to the image wrapper or the image itself for custom styling.         |
| `fieldOptions` | `Object`           | V3+ image field configuration such as `preview`, `hideInput`, `browse`, and `autocomplete`. |

## `fieldOptions`

Common keys include:

| Key            | Type      | Description                                              |
| -------------- | --------- | -------------------------------------------------------- |
| `preview`      | `Boolean` | When not `false`, shows the preview area below the field |
| `hideInput`    | `Boolean` | When `true`, hides the URL/text input                    |
| `browse`       | `Boolean` | When `false`, hides the file picker input                |
| `autocomplete` | `String`  | Browser autocomplete mode for the text input             |

### Example Schema Snippet (Image from URL)

```json
{
  "type": "image",
  "label": "Company Logo",
  "model": "companyLogoUrl",
  "fieldOptions": {
    "preview": true
  }
}
```

### Example Schema Snippet (Image from Base64 Data)

```json
{
  "type": "image",
  "model": "userAvatarBase64",
  "styleClasses": "img-circle",
  "fieldOptions": {
    "preview": true,
    "hideInput": true
  }
}
```

## BetterForms Specific Notes

* If the text input is used, only values starting with `http` are written back to the model.
* If the file input is used, the selected file is read with `FileReader.readAsDataURL()` and stored as a base64 data URL in the model.
* The preview area also includes a remove control that clears the model value.
* For heavier upload pipelines, use the dedicated upload components documented under "Uploading Files".

## Full Property Reference

This element is implemented in the sibling `vue-form-generator` source as `fieldImage.vue`.


# Input

The live `input` field is the standard single-line field for text, numbers, dates, email, passwords, and similar browser input types.

## Common Configuration Properties

| Property       | Type               | Description                                                                  |
| -------------- | ------------------ | ---------------------------------------------------------------------------- |
| `type`         | `String`           | Must be `"input"`                                                            |
| `label`        | `String`           | Field label                                                                  |
| `model`        | `String`           | Model key that stores the value                                              |
| `placeholder`  | `String`           | Placeholder text                                                             |
| `disabled`     | `Boolean`          | Disables the field                                                           |
| `readonly`     | `Boolean`          | Makes the field read-only                                                    |
| `required`     | `Boolean`          | Marks the field as required                                                  |
| `styleClasses` | `String` / `Array` | CSS classes for the field wrapper                                            |
| `fieldOptions` | `Object`           | V3+ input configuration, including `inputType` and standard input attributes |

## `fieldOptions`

The live V3+ field reads its HTML input configuration from `fieldOptions`.

Common keys include:

| Key            | Type                | Description                                                                                                                   |
| -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `inputType`    | `String`            | Browser input type such as `text`, `password`, `email`, `number`, `url`, `tel`, `date`, `datetime-local`, `color`, or `range` |
| `autocomplete` | `String`            | Browser autocomplete mode                                                                                                     |
| `min`          | `Number` / `String` | Minimum value for numeric/date-like inputs                                                                                    |
| `max`          | `Number` / `String` | Maximum value for numeric/date-like inputs                                                                                    |
| `step`         | `Number` / `String` | Step increment for numeric/range inputs                                                                                       |
| `maxlength`    | `Number`            | Maximum length for text-like inputs                                                                                           |
| `minlength`    | `Number`            | Minimum length for text-like inputs                                                                                           |
| `pattern`      | `String`            | HTML input pattern                                                                                                            |
| `accept`       | `String`            | Accepted file types when using file-style inputs                                                                              |

## Example Schema Snippet

```json
{
  "type": "input",
  "label": "Full Name",
  "model": "fullName",
  "placeholder": "Enter your full name",
  "required": true,
  "hint": "Please provide your first and last name.",
  "fieldOptions": {
    "inputType": "text",
    "maxlength": 100
  }
}
```

## Number Input Example

```json
{
  "type": "input",
  "label": "Quantity",
  "model": "itemQuantity",
  "default": 1,
  "hint": "Enter a number between 1 and 100.",
  "fieldOptions": {
    "inputType": "number",
    "min": 1,
    "max": 100,
    "step": 1
  }
}
```

## Runtime Notes

* `fieldOptions.inputType` is the canonical V3+ shape. BetterForms can migrate legacy top-level `inputType`, but docs should prefer `fieldOptions`.
* For `number` and `range`, the field parses user input into numeric values.
* For `date`, `datetime`, and `datetime-local`, the field applies date parsing and formatting logic before writing back to the model.
* If `schema.format` is supplied for date-like input types, it is used when formatting the stored value.

## Full Property Reference

This element is implemented in the sibling `vue-form-generator` source as `fieldInput.vue`.


# Masked Input

The live `masked` field uses the jQuery Masked Input plugin.

## Common Configuration Properties

| Property                   | Type      | Description                                           |
| -------------------------- | --------- | ----------------------------------------------------- |
| `type`                     | `String`  | Must be `"masked"`                                    |
| `label`                    | `String`  | Field label                                           |
| `model`                    | `String`  | Model key that stores the input value                 |
| `placeholder`              | `String`  | Placeholder text                                      |
| `disabled`                 | `Boolean` | Disables the input                                    |
| `readonly`                 | `Boolean` | Makes the input read-only                             |
| `fieldOptions.mask`        | `String`  | Mask pattern passed to `$(el).mask(...)`              |
| `fieldOptions.maskOptions` | `Object`  | Optional plugin options passed as the second argument |

## Example Schema Snippet

```json
{
  "type": "masked",
  "label": "Phone Number",
  "model": "userPhoneNumber",
  "placeholder": "(999) 999-9999",
  "fieldOptions": {
    "mask": "(999) 999-9999"
  }
}
```

## Runtime Notes

* The component requires the jQuery Masked Input library to be loaded globally.
* On mount, the component runs `unmask().mask(fieldOptions.mask, fieldOptions.maskOptions)`.
* On destroy, it removes the mask with `unmask()`.

## Full Property Reference

This element is implemented in the sibling `vue-form-generator` source as `fieldMasked.vue` and uses the jQuery Masked Input plugin.


# Range Slider (noUiSlider)

The Range Slider element allows users to select a single value or a range of values by sliding handles along a track. It's highly customizable and suitable for selecting numeric ranges, like price, age, or percentages.

In BetterForms, this element leverages the `noUiSlider` library to provide a touch-friendly and flexible range selection interface.

## Common Configuration Properties

| Property       | Type               | Description                                                                                                    |
| -------------- | ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `type`         | `String`           | Must be set to `"noUiSlider"`.                                                                                 |
| `label`        | `String`           | The label for the range slider.                                                                                |
| `model`        | `String`           | The key in your BetterForms data model where the selected value or array of values (for range) will be stored. |
| `disabled`     | `Boolean`          | If `true`, the slider will be visible but not interactive. Defaults to `false`.                                |
| `readonly`     | `Boolean`          | If `true`, the slider value cannot be changed. Similar to `disabled`. Defaults to `false`.                     |
| `hint`         | `String`           | Additional helper text displayed below the slider.                                                             |
| `styleClasses` | `String` / `Array` | CSS classes for styling the wrapper.                                                                           |
| `fieldOptions` | `Object`           | **Crucial property.** Options passed directly into `window.noUiSlider.create(...)`.                            |

### `fieldOptions` common settings:

| Option        | Type                           | Description                                                                                                             |
| ------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `start`       | `Number` / `Array`             | Initial value(s) of the slider. A single number for one handle, an array of two numbers for a range (e.g., `[20, 80]`). |
| `range`       | `Object`                       | Defines the slider's `min` and `max` values. E.g., `{ 'min': 0, 'max': 100 }`.                                          |
| `connect`     | `Boolean` / `Array` / `String` | Highlights the area between handles or from an edge to a handle. `true` for a range, `[true, false]` for lower connect. |
| `step`        | `Number`                       | The increment/decrement step size.                                                                                      |
| `orientation` | `String`                       | Orientation of the slider: `"horizontal"` (default) or `"vertical"`.                                                    |
| `tooltips`    | `Boolean` / `Array` / `Object` | Whether to show tooltips above the handles. Can be `true`, or an array of formatter objects.                            |
| `format`      | `Object`                       | An object with `to` and `from` functions for formatting the displayed values (e.g., adding units like '$' or '%').      |
| `behaviour`   | `String`                       | User interaction behavior, e.g., `'tap-drag'`, `'drag'`.                                                                |
| `pips`        | `Object`                       | Configuration for displaying pips (markers) along the slider.                                                           |

### Example Schema Snippet (Single Value Slider)

```json
{
  "type": "noUiSlider",
  "label": "Set Volume",
  "model": "volumeLevel",
  "fieldOptions": {
    "min": 0,
    "max": 100,
    "step": 1,
    "tooltips": true,
    "connect": [true, false]
  }
}
```

### Example Schema Snippet (Range Slider)

```json
{
  "type": "noUiSlider",
  "label": "Price Range",
  "model": "priceRangeArray",
  "fieldOptions": {
    "min": 0,
    "max": 1000,
    "double": true,
    "step": 10,
    "connect": true,
    "tooltips": true,
    "format": {
      "to": function (value) { return '$' + parseInt(value); },
      "from": function (value) { return Number(value.replace('$', '')); }
    }
  }
}
```

## BetterForms Specific Notes

* The component requires the `noUiSlider` library to be loaded globally.
* The model stores a single number for one handle, or a two-item number array for a range.
* If the model already has a value, that value is used as the slider `start`.
* If there is no current model value, the component builds `start` from `fieldOptions.min`, and uses `[min, min]` when `fieldOptions.double` is present.
* `fieldOptions.pips` and `fieldOptions.tooltips` also affect wrapper classes used for spacing.

## Full Property Reference

This element wraps the `noUiSlider` library. For the available `fieldOptions`, refer to:

* [VFG noUiSlider Field Documentation](https://vue-generators.gitbook.io/vue-generators/fields/optional-fields/nouislider)
* [noUiSlider Official Documentation](https://refreshless.com/nouislider/)


# Radios

The Radios element presents a group of options where only one option can be selected at a time. This is commonly used for mutually exclusive choices.

In BetterForms, this is suitable for questions where the user must pick a single answer from a small, fixed set of options (e.g., gender, a rating scale, or a single preference).

## Common Configuration Properties

Below are some of the most common properties used when configuring a Radios element in BetterForms:

| Property       | Type               | Description                                                                                                                                                  |
| -------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type`         | `String`           | Must be set to `"radios"`.                                                                                                                                   |
| `label`        | `String`           | A general label for the entire radio button group.                                                                                                           |
| `model`        | `String`           | The key in your BetterForms data model where the value of the selected radio button will be stored.                                                          |
| `values`       | `Array`            | An array of strings or objects defining the individual radio button options. If objects, they usually have `name` (display) and `value` (stored) properties. |
| `disabled`     | `Boolean`          | If `true`, all radio buttons in the group will be visible but not interactive. Defaults to `false`.                                                          |
| `hint`         | `String`           | Additional helper text displayed with the radio button group.                                                                                                |
| `required`     | `Boolean`          | If `true`, the form will require one option to be selected for submission. Defaults to `false`.                                                              |
| `featured`     | `Boolean`          | Can be used by themes to apply special styling to the radio group.                                                                                           |
| `styleClasses` | `String` / `Array` | CSS class(es) to apply to the radio group wrapper for custom styling.                                                                                        |

### Example Schema Snippet

```json
{
  "type": "radios",
  "label": "Preferred contact method:",
  "model": "contactPreference",
  "values": [
    { "name": "Email", "value": "email" },
    { "name": "Phone", "value": "phone" },
    { "name": "Mail", "value": "mail" }
  ],
  "default": "email",
  "hint": "Select only one option."
}
```

## BetterForms Specific Notes

* The `model` property will store the `value` of the selected radio button option.
* Ensure the `values` array is properly defined to present all necessary choices to the user.
* For a large number of single-choice options, a `select` (dropdown) element might offer a more compact UI.

## Full Property Reference

This element is based on a standard form generation library. For a comprehensive list of all available properties, advanced configuration options, and the full technical specification, please refer to the [Radios Field Documentation](https://vue-generators.gitbook.io/vue-generators/fields/core-fields/radios).


# Select

The Select element, commonly known as a dropdown list, allows users to choose a single option from a predefined list. It's a compact way to present multiple choices when only one can be selected.

In BetterForms, this is frequently used for selecting a country, state, category, status, or any other scenario where a user needs to pick one item from a moderately long list of options.

## Common Configuration Properties

Below are some of the most common properties used when configuring a Select element in BetterForms:

| Property       | Type               | Description                                                                                                                                                                                                 |
| -------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`         | `String`           | Must be set to `"select"`.                                                                                                                                                                                  |
| `label`        | `String`           | A label for the dropdown list.                                                                                                                                                                              |
| `model`        | `String`           | The key in your BetterForms data model where the value of the selected option will be stored.                                                                                                               |
| `values`       | `Array`            | An array of strings or objects defining the options in the dropdown. If objects, they usually have `name` (display text) and `id` or `value` (stored value) properties. Grouped items can also be provided. |
| `disabled`     | `Boolean`          | If `true`, the select element will be visible but not interactive. Defaults to `false`.                                                                                                                     |
| `hint`         | `String`           | Additional helper text displayed with the select element.                                                                                                                                                   |
| `required`     | `Boolean`          | If `true`, the form will require an option to be selected. Defaults to `false`.                                                                                                                             |
| `featured`     | `Boolean`          | Can be used by themes to apply special styling.                                                                                                                                                             |
| `styleClasses` | `String` / `Array` | CSS class(es) to apply to the select element wrapper for custom styling.                                                                                                                                    |
| `fieldOptions` | `Object`           | Advanced options for the V3+ field shape, including `noneSelectedText`, `hideNoneSelectedText`, and custom object key mapping with `value` / `name`.                                                        |

### Example Schema Snippet

```json
{
  "type": "select",
  "label": "Select your country:",
  "model": "userCountry",
  "values": [
    { "id": "US", "name": "United States" },
    { "id": "CA", "name": "Canada" },
    { "id": "GB", "name": "United Kingdom" },
    { "id": "AU", "name": "Australia" }
  ],
  "fieldOptions": {
    "noneSelectedText": "-- Please select a country --"
  },
  "hint": "Choose your country of residence."
}
```

## BetterForms Specific Notes

* The `model` stores the chosen option value.
* For object items, the V3+ field uses `fieldOptions.value` and `fieldOptions.name` when you want keys other than the default `id` and `name`.
* The `values` array can also include `group` on object items, which the runtime renders as `<optgroup>` sections.
* For very long lists of options that might benefit from searching or type-ahead functionality, consider more advanced select components like `vueMultiSelect` (an optional VFG field) or custom solutions if available in BetterForms.

## Full Property Reference

This element is based on a standard form generation library. For a comprehensive list of all available properties, advanced configuration options (like optgroups), and the full technical specification, please refer to the [Select Field Documentation](https://vue-generators.gitbook.io/vue-generators/fields/core-fields/select).


# Advanced Select (selectEx)

The live `selectEx` field uses the Bootstrap Select (`selectpicker`) jQuery plugin, not `vue-multiselect`.

## Common Configuration Properties

| Property       | Type                 | Description                                                                   |
| -------------- | -------------------- | ----------------------------------------------------------------------------- |
| `type`         | `String`             | Must be `"selectEx"`                                                          |
| `label`        | `String`             | Field label                                                                   |
| `model`        | `String`             | Model key that stores the selected value                                      |
| `values`       | `Array` / `Function` | Available choices. If a function is used, it is called with `(model, schema)` |
| `placeholder`  | `String`             | Placeholder/title shown by the selectpicker                                   |
| `disabled`     | `Boolean`            | Disables the select                                                           |
| `fieldOptions` | `Object`             | Options passed into `$(el).selectpicker(...)`                                 |

## `values` and Object Items

If `values` contains objects:

* `fieldOptions.value` defines which key becomes the stored value
* `fieldOptions.name` defines which key is shown as the option label

If those keys are omitted, the component falls back to:

* `id` for the stored value
* `name` for the visible label

## Multi-Select

Set:

```json
{
  "fieldOptions": {
    "multiSelect": true
  }
}
```

This maps to the rendered select element's `multiple` attribute.

## Example Schema Snippet

```json
{
  "type": "selectEx",
  "label": "Choose a Category",
  "model": "selectedCategory",
  "values": [
    { "id": 1, "name": "Electronics" },
    { "id": 2, "name": "Books" },
    { "id": 3, "name": "Clothing" }
  ],
  "placeholder": "Search or select a category",
  "fieldOptions": {
    "value": "id",
    "name": "name"
  }
}
```

## Runtime Notes

* The component requires the Bootstrap Select library to be loaded globally.
* The field refreshes the selectpicker UI when the model changes.
* For single-select mode, the component renders a blank option with `null` value unless `multiSelect` is enabled.

## Full Property Reference

This element is implemented in the sibling `vue-form-generator` source as `fieldSelectEx.vue` and uses the Bootstrap Select jQuery plugin.


# TextArea

The TextArea element provides a multi-line plain-text editing control, suitable for capturing longer pieces of text like comments, descriptions, or detailed notes.

In BetterForms, this is the standard choice when you need users to input more than a single line of text.

## Common Configuration Properties

Below are some of the most common properties used when configuring a TextArea element in BetterForms:

| Property       | Type               | Description                                                                                      |
| -------------- | ------------------ | ------------------------------------------------------------------------------------------------ |
| `type`         | `String`           | Must be set to `"textArea"`.                                                                     |
| `label`        | `String`           | The text displayed above or next to the textarea to describe its purpose.                        |
| `model`        | `String`           | The key in your BetterForms data model where the textarea's string value will be stored.         |
| `placeholder`  | `String`           | Placeholder text displayed within the textarea when it is empty.                                 |
| `disabled`     | `Boolean`          | If `true`, the textarea will be visible but not interactive. Defaults to `false`.                |
| `readonly`     | `Boolean`          | If `true`, the textarea content will not be editable. Defaults to `false`.                       |
| `hint`         | `String`           | Additional helper text displayed with the textarea.                                              |
| `required`     | `Boolean`          | If `true`, the form will require this field to have a value for submission. Defaults to `false`. |
| `featured`     | `Boolean`          | Can be used by themes to apply special styling.                                                  |
| `styleClasses` | `String` / `Array` | CSS class(es) to apply to the textarea wrapper for custom styling.                               |
| `fieldOptions` | `Object`           | V3+ textarea configuration such as `rows`, `min`, `max`, and `autoExpand`.                       |

### fieldOptions

The following options can be placed inside the `fieldOptions` object on the schema:

| Property     | Type      | Description                                                                                                                                                                       |
| ------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rows`       | `Number`  | Number of visible text rows (default `2`).                                                                                                                                        |
| `min`        | `Number`  | Minimum character length (`minlength` attribute).                                                                                                                                 |
| `max`        | `Number`  | Maximum character length (`maxlength` attribute).                                                                                                                                 |
| `autoExpand` | `Boolean` | When `true`, the textarea automatically grows taller as the user types so all content is visible without scrolling. Defaults to `false`.                                          |
| `maxHeight`  | `Number`  | Optional pixel limit for auto-expansion. Once the content exceeds this height the textarea stops growing and shows a scrollbar instead. Only applies when `autoExpand` is `true`. |

### Example Schema Snippet

```json
{
  "type": "textArea",
  "label": "Additional Comments",
  "model": "userComments",
  "placeholder": "Enter any additional comments here...",
  "hint": "Maximum 500 characters.",
  "fieldOptions": {
    "rows": 4,
    "max": 500
  }
}
```

### Auto-Expand Example

Use `autoExpand` to make the textarea grow with its content. Add `maxHeight` to cap the expansion at a fixed pixel height:

```json
{
  "type": "textArea",
  "label": "Biography",
  "model": "bio",
  "placeholder": "Tell us about yourself...",
  "fieldOptions": {
    "rows": 4,
    "max": 500,
    "autoExpand": true,
    "maxHeight": 240
  }
}
```

When `autoExpand` is enabled:

* The textarea starts at the height defined by `rows`.
* As the user types and content exceeds the visible area, the textarea grows to fit.
* If `maxHeight` is set (in pixels), the textarea stops growing at that height and a vertical scrollbar appears for any additional content.
* When content is deleted, the textarea shrinks back down automatically.

## BetterForms Specific Notes

* This element is intended for plain text. If you need rich text editing (WYSIWYG), look for a dedicated rich text editor component in BetterForms or consider integrating one as a custom element.
* In the live V3+ field, character limits are read from `fieldOptions.min` and `fieldOptions.max`, which map to `minlength` and `maxlength`.
* **v3.3.0+**: The `autoExpand` and `maxHeight` options were added. These require VFG v3.3.0 or later.

## Full Property Reference

This element is based on a standard form generation library. For a comprehensive list of all available properties, advanced configuration options, and the full technical specification, please refer to the [TextArea Field Documentation](https://vue-generators.gitbook.io/vue-generators/fields/core-fields/textArea).


# Common


# Input

The input field in our editor is customizable and supports various browser input types. In V3+ schemas, those options should be placed under `fieldOptions`.

#### **1. Input Field Structure**

Each input field is defined by a JSON object with the following properties:

* **`"fieldOptions.inputType"`**: Type of input (`text`, `password`, `email`, `number`, etc.).
* **`"label"`**: Descriptive text for the input field.
* **`"model"`**: Field name in the data model.
* **`"styleClasses"`**: CSS classes for layout.
* **`"type"`**: Always `"input"` for input fields.

#### **2. Available Input Types**

Here are examples for common input types:

**Text Input:**

```json
{
  "fieldOptions": {
    "inputType": "text"
  },
  "label": "Username",
  "model": "username",
  "styleClasses": "col-md-3",
  "type": "input"
}
```

**Password Input:**

```json
{
  "fieldOptions": {
    "inputType": "password",
    "minlength": 6
  },
  "label": "Password",
  "model": "password",
  "hint": "Minimum 6 characters",
  "validator": "calc",
  "validator_calc": "model.password == model.password2",
  "styleClasses": "col-md-6",
  "type": "input"
}
```

**Email Input:**

```json
{
  "fieldOptions": {
    "inputType": "email"
  },
  "label": "Email",
  "model": "email",
  "validator": "email",
  "required": true,
  "styleClasses": "col-md-3",
  "type": "input"
}
```

**Number Input:**

```json
{
  "fieldOptions": {
    "inputType": "number",
    "step": 1
  },
  "label": "Age",
  "model": "age",
  "styleClasses": "col-md-3",
  "type": "input"
}
```

#### **3. Validation**

Use the `"validator"` property to validate the input. Common validators include:

* **email**: Ensures the input is a valid email address.
* **calc**: Custom validation using `"validator_calc"`.


# Button

This page showcases various buttons you can implement in your app, each designed for specific actions. Below is a detailed explanation of the different button types, their configurations, and their intended actions.

{% hint style="info" %}
To see buttons in action, check out [examples](https://app.fmbetterforms.com/#/apps/pages/edit?id=5251675D-4A4D-4FE1-AD35-5D5B038CA924) on this [page](https://examplesdev.fmbetterforms.com/#/form/5251675D-4A4D-4FE1-AD35-5D5B038CA924).
{% endhint %}

### Regular Buttons

The general structure of a button object is as follows:

```json
{
  "actions": [],
  "buttonClasses": "btn btn-info",
  "styleClasses": "col-md-2",
  "text": "Push Me",
  "type": "button"
}
```

To assign actions to a button, you can populate the <mark style="color:red;">`actions`</mark> array. Below are examples of common button actions.

#### **Open a Modal**

```json
{
    "actions": [{
        "action": "showModal",
        "options": {
            "body": "This is the modal body, actions are Kewl!",
            "icon": "success",
            "title": "<br>You triggered a modal!"
        }
    }],
    "buttonClasses": "btn btn-info btn-lg",
    "icon": "fa fa-comment",
    "styleClasses": "col-md-2",
    "text": "Show a Modal",
    "type": "button"
}
```

#### **Redirect to a Path**

* **Internal Link**

  ```json
  {
      "actions": [{
          "action": "path",
          "options": {
              "path": "/dash"
          }
      }],
      "buttonClasses": "btn btn-info",
      "hint": "This redirects the user to another form",
      "styleClasses": "col-md-2",
      "text": "Goto dash",
      "type": "button"
  }
  ```
* **External Link**

  ```json
  {
      "actions": [{
          "action": "path",
          "options": {
              "url": "https://docs.fmbetterforms.com/"
          }
      }],
      "buttonClasses": "btn btn-danger btn-xl btn-trans btn-pill",
      "hint": "Styled with CSS, opens a new tab",
      "styleClasses": "col-md-2",
      "text": "Show Documentation",
      "type": "button"
  }
  ```

#### **Show Alert**

```json
{
    "actions": [{
        "action": "showAlert",
        "options": {
            "text": "This is the Alert body, actions are Kewl!",
            "title": "Hello World!",
            "type": "error"
        }
    }],
    "buttonClasses": "btn btn-primary btn-block",
    "hint": "Fills the column it defines",
    "icon": "fa fa-triangle-exclamation",
    "styleClasses": "col-md-4",
    "text": "Full Width showAlert",
    "type": "button"
}
```

#### JavaScript Function

The Print button action executes JavaScript functions:

```json
{
    "actions": [{
        "action": "function",
        "function": "window.print()",
        "options": {}
    }],
    "buttonClasses": "btn btn-success btn-trans",
    "icon": "fa fa-print",
    "styleClasses": "col-md-2",
    "text": "Print",
    "type": "button"
}
```

#### runUtilityHook

This action allows a button click to call the scoped FileMaker <mark style="color:red;">`onUtility`</mark> server hook. You can pass additional parameters in the <mark style="color:red;">`options`</mark> object:

```json
{
    "actions": [{
        "action": "runUtilityHook",
        "options": {
            "type": "save"
        }
    }],
    "buttonClasses": "btn btn-info",
    "hint": "to save...",
    "styleClasses": "col-md-2",
    "text": "Save",
    "type": "button"
}
```

#### Dropdown Buttons

Dropdown buttons contain multiple button objects within the `subs` array. Here’s an example:

```json
{
    "buttonClasses": "btn btn-info btn-trans",
    "icon": "fa fa-comment",
    "label": "Dropdown Menu",
    "styleClasses": "col-md-4",
    "subs": [{
        "actions": [],
        "text": "Button 1"
    }, {
        "divider": true
    }, {
        "actions": [],
        "text": "Button 2"
    }],
    "text": "Some Actions",
    "type": "button"
}
```

### Button Groups

Button groups use the <mark style="color:red;">`group`</mark> array to bundle related buttons together.

```json
{
    "group": [{
        "actions": [],
        "buttonClasses": "btn btn-success btn-trans",
        "icon": "fa fa-plus",
        "text": "Add",
        "type": "button"
    }, {
        "actions": [],
        "buttonClasses": "btn btn-success btn-trans",
        "icon": "fa fa-archive",
        "text": "Save",
        "type": "button"
    }],
    "groupClasses": "btn-group",
    "styleClasses": "col-md-6",
    "type": "button"
}
```

### Dynamic Styling

This button uses <mark style="color:red;">`buttonClasses_calc`</mark> to dynamically generate CSS classes. It is a computed property that combines static classes with dynamic model data:

```json
{
    "actions": [],
    "buttonClasses_calc": "'p-4 w-48 text-center border font-bold rounded-lg ' + model.buttonStyleClasses",
    "styleClasses": "col-md-3",
    "text": "Push Me",
    "type": "button"
}
```

### Button Triggering a namedActionn

This button uses the <mark style="color:red;">`onclick_actions`</mark> array to call a <mark style="color:red;">`namedAction`</mark> when clicked:

```json
{
    "buttons": [{
        "classes": "btn btn-success btn-trans",
        "label": "Do Something",
        "onclick_actions": [{
            "action": "namedAction",
            "name": "SomethingClicked",
            "options": {}
        }]
    }],
    "label": "My Input",
    "model": "field1",
    "styleClasses": "col-md-3",
    "fieldOptions": {
        "inputType": "text"
    },
    "type": "input"
}
```


# Data Table

{% hint style="info" %}
This component is based on [Vue-Tables-2](https://matanya.gitbook.io/vue-tables-2/options-api#options) Refer to source code for additional documentation.
{% endhint %}

## Additional keys:

|      Additional Keys |   Type   | Description                                                                        |
| -------------------: | :------: | ---------------------------------------------------------------------------------- |
|              `model` | *string* | The key of an array in your data model containing the data to display in the table |
|            `options` | *object* | Various options as defined in the source code *(see link above)*                   |
| `actions_onRowClick` |  *array* | (optional) Array of actions to execute when row is clicked                         |
|              `slots` |  *array* | (optional) Array of objects, allows insertion of HTML content *(see below)*        |

## Slots Example

Add the `slots` key to this element to define HTML regions to be displayed with the table. The `slot` key within each object defines the name of the slot, which can be referenced in the `columns` key.

If you name a slot `child_row`, that HTML content will be displayed when the row of the table is expanded. *(see CSS trick* [*below*](#child-rows) *for how to customize the look of the child row icon)*

```yaml
"columns": ["name", "title", "slot_name", "button_slot"],
"slots": [{
    "html": "<h1>{{model.title}}</h1>",
    "slot": "title"
}, {
    "html": "<h6>{{model.description}}</h6>",
    "slot": "slot_name"
}, {
    "html": "<button class=\"btn btn-info\" v-on:click=\"namedAction('myNamedAction'; {row: props.row})\"><i class=\"fa fa-check\"></i> OK</button>",
    "slot": "button_slot"
}, {
    "html": "{{props.row.productList}}",
    "slot": "child_row"
}]
```

{% hint style="info" %}
When referencing row data within slots, use `props.row` instead of the usual `model` keyword.
{% endhint %}

## Slots

Slots allow you to insert you own custom HTML in predefined positions within the component Slots with respect the Klai Forms render engine and render VueJS code also:

* `beforeTable`: Before the table wrapper. After the controls row
* `afterTable`: Before the table wrapper.
* `beforeFilter`: Before the global filter (`filterByColumn: false`)
* `afterFilter`: After the global filter
* `beforeLimit`: Before the per page control
* `afterLimit`: After the per page control
* `beforeFilters`: Before the filters row (`filterByColumn: true`)
* `afterFilters`: After the filters row
* `beforeBody`: Before the `<tbody>` tag
* `afterBody`: After the `<tbody>` tag
* `prependBody`: Prepend to the `<tbody>` tag
* `appendBody`: Append to the `<tbody>` tag

If a slot has the same name as a column, it will replace the columns contents. You can class the rows object (data object for that row) via `model.row.myField`

## Accessing Data in slots

### row

Each rows data can be found in the `props.row` variable when using custom html slots.

eg: `{{props.row.nameFirst}}` would render the first name field

### data model

Sometimes you may want to reference the parent data model (The model that was used for the form, or the container element). You can reference to parents data model with the variable `model`

eg: `{{model.isLocked}}` would render the isLocked field in the parent data model

## Child Rows

Add the following CSS to your site to change how the icon to open or close the child row

```css
.VueTables__child-row-toggler {

    width: 16px;

    height: 16px;

    line-height: 16px;

    display: block;
    margin: auto;

    text-align: center;
}



.VueTables__child-row-toggler--closed::before {
    content: "►";
}

.VueTables__child-row-toggler--open::before {

    content: "▼";
}
```

### Interacting with the Table (Row Click Actions)

**actions\_onRowClick**

The **Data Table** element supports `actions_onRowClick` actions. This allows you to programmatically control what happens when a user clicks a row.

Within each action in this actions array, a "row" key will be injected into the `options.params` so that you can reference the data of the row that was clicked.

* To access the row from a function action use `action.options.params.row`
* To access the row from within an options key (see example below), use `this.params.row.id`

The following is an example that will pas the key `id` from the table row into the path action

```yaml
// actions_onRowClick is added to the root level of the element 

// Best practice:
"actions_onRowClick": [{
   "action": "namedAction",
   "name": "myRowClickHandler"
  }]  
  
 // in myRowClickHandler, you can get the row from params:
 let row = action.options.params.row
 ...
 
 
 
 // Older practice:
 actions_onRowClick": [{
   "action": "path",
   "options": {
      "path_calc": "'/invoicedetail?id=' + this.params.row.id"
    }
  ]
  
// In Filemaker in the invoicedetail onFormRequest hook script
// Get the id via $$BF_Query:
Set Variable $id = JSONGetElement ( $$BF_Query ; "id" )

```

## Custom Sorting Functionality

A common issue arises when attempting to use the table’s custom sorting functionality for columns where numeric values are stored as strings. For instance, when sorting a column with prices stored as strings, the default behavior sorts them alphabetically rather than numerically.

To address this, you can define custom sorting functions in the table options. Below is an example demonstrating how to handle custom sorting for different types of columns:

* **price\_function**: Sorts the `price` column numerically, converting the price strings to floats for accurate comparison.
* **name\_function**: Sorts the `name` column by last name, splitting the full name string and comparing the last names.
* **date\_function**: Sorts the `date` column chronologically, converting the date strings to date objects for accurate comparison.

```json
"options": {
    "customSorting": {
        "price_function": "var ascending = arguments[0]; return function(a,b){if (ascending) return parseFloat(a.price) >= parseFloat(b.price) ? 1 : -1; return parseFloat(a.price) <= parseFloat(b.price) ? 1 : -1;}",
        "name_function": "var ascending = arguments[0]; return function(a,b){if (ascending) return a.name.split(' ')[1] >= b.name.split(' ')[1] ? 1 : -1; return a.name.split(' ')[1] <= b.name.split(' ')[1] ? 1 : -1;}",
        "date_function": "var ascending = arguments[0];return function(a, b) {if (ascending) return moment(a.date, 'M/D/YYYY').isAfter(moment(b.date, 'M/D/YYYY')) ? 1 : -1;return moment(b.date, 'M/D/YYYY').isAfter(moment(a.date, 'M/D/YYYY')) ? 1 : -1;}"
    }
}

```

{% hint style="info" %}
**Note on Custom Sort Functions**

When providing a custom sort function in JSON (e.g., via the `function` key), the entire function **must be written on a single line** without any line breaks (, line returns, or multi-line formatting). This is required for the function to compile correctly in the BetterForms runtime environment.
{% endhint %}

✅ Valid:

```json
"function": "return a.value - b.value;"
```

❌ Invalid (Will cause a compile error):

```json
"function": "return a.value -\n b.value;"
```


# HTML

The HTML element allows you to merge any HTML and inline CSS into your form layout. HTML source data can be in either (or both) the data `model` or the field `scmema`

| Key            | Value(s)   | Type   | Description                                                                                                                                                                                                                                            |
| -------------- | ---------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| type           | html       | string |                                                                                                                                                                                                                                                        |
| html           |            | string | raw HTML. if both a model and `html` code are supplied, the `html` code goes first.                                                                                                                                                                    |
| model          |            | string | raw HTML (VueJS) code                                                                                                                                                                                                                                  |
| dataModelScope | {optional} | string | if defined, the scope of the `model` object within the html will root to this data model path. This is used when you are building things that edit other things and want live data rendering. This is used in the HTML builder page of the admin site. |

### Example

```yaml
// you can use either body or model or both keys for HTML source code

{
  "html": "<h1>This is some HTML!</h1> It will display ahead of the model HTML",
  "model": "mySourceHtml",
  "type": "html",
  "styleClasses": "col-md-12"
},
```

Use of the model key for this element is helpful if you have HTML content that you want to generate in FileMaker and push to the browser. Otherwise, it can be left out entirely and you can build your own HTML elements using the [HTML editor](#html-editor) and [VueJS Template syntax](https://github.com/DelfsEngineering/fm-betterforms/blob/master/guides/html-and-vuejs.md).

## HTML Editor

This powerful element allows you to add custom HTML and [VueJS Template syntax](https://github.com/DelfsEngineering/fm-betterforms/blob/master/guides/html-and-vuejs.md).

#### Opening the HTML Editor

!["Open HTML Editor button"](/files/-Ljraw7c-DEB2IKeYmps)

When using [VueJS Template syntax](https://github.com/DelfsEngineering/fm-betterforms/blob/master/guides/html-and-vuejs.md), you'll see your [development data model](/reference/form-settings/data-model) displayed in the preview and can edit it in the Data Model tab.

!["HTML Editor"](/files/tY6HVxzYLM06GqGPJybq)

This makes developing with custom code fast and easy.

{% hint style="info" %}
**Remember:** The HTML editor works wherever there is a "html" key in the schema! This expands beyond the HTML element to [modals](/reference/actions-processor/actions_overview/showmodal) and [slots](/reference/components-overview/common/tables2#slots) of a [data table](/reference/components-overview/common/tables2).
{% endhint %}


# Grouping Elements

Grouping elements are elements that can contain other elements. The most common groups are **Tabs** and **panels** which are useful for organizing elements in your form. However you can also use elements to show a varied amount of data.

* [Accordions](/reference/components-overview/grouping-elements/accordion)
* [Listrows](/reference/components-overview/grouping-elements/listrows)

See also:

* Groups


# Tabs

Organize complex forms using a standard tab element

## Tab Modes

A tabs element can be constructed in either **Data Mode** or **Form Mode**. **Form mode** allows you to define each tab individually as its own form, very similar to how a tab element would work in FileMaker. **Data Mode** takes an array of data and generates a tab for each item in the array, with identical elements in each tab containers.

* To use Data Mode, set `"type": "vtabs"`
* To use Form Mode set `"type": "tabs_form2"` (preferred) or `"type": "tabs_form"`

Otherwise, all other parts of this page apply to both modes.

Additional Keys

|                       | Type          | Description                                                                   |
| --------------------- | ------------- | ----------------------------------------------------------------------------- |
| onTabClicked\_actions | actions array | this key accepts an array of actions that are executed when a tab is clicked. |

## Form Mode style classes (`tabs_form2`)

These keys control CSS classes on different parts of the tabs element. They are part of **basecode** (available to all apps once that basecode version is deployed).

| Key                     | Level        | Description                                                                                                                                                                                                                                                                 |
| ----------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `styleClasses`          | field        | Classes on the outer VFG form-group around the tabs field.                                                                                                                                                                                                                  |
| `styleClassesWrapper`   | field        | Classes on the outer tab wrapper (`wrapper tab-wrapper panel panel-default` by default). **Overwrites** the default classes when set; omit to keep legacy defaults. Use this for height/flex layout (e.g. `flex flex-col flex-1 h-full`). *Available in basecode v3.5.58+*. |
| `styleClassesTabs`      | field        | Classes on the `<ul>` that wraps all tab labels.                                                                                                                                                                                                                            |
| `styleClassesTab`       | field or tab | Classes on each tab label (`<li>`). Per-tab value overrides the field default.                                                                                                                                                                                              |
| `styleClassesTabActive` | field or tab | Classes added to the active tab label. Per-tab value overrides the field default.                                                                                                                                                                                           |
| `styleClassesBody`      | field        | Classes on the tab content container around the panes.                                                                                                                                                                                                                      |
| `styleClasses`          | tab          | Classes on that tab’s inner pane (`vue-form-generator`). Set on each object inside the tabs `schema` array.                                                                                                                                                                 |
| `align`                 | field        | Adds `tab-top`, `tab-left`, etc. on the wrapper (separate from `styleClassesWrapper`).                                                                                                                                                                                      |

### Height / flex tip

To make tabs fill parent height, put flex/height classes on the wrapper:

```json
{
  "type": "tabs_form2",
  "styleClasses": "h-full mb-0 relative",
  "styleClassesWrapper": "flex flex-col flex-1 h-full",
  "styleClassesBody": "flex flex-col flex-1 p-2 mb-0",
  "align": "top",
  "schema": [
    {
      "tabLabel": "Model",
      "styleClasses": "flex-1 h-full",
      "fields": []
    }
  ]
}
```

Vertical Tabs give you the ability to contain repeating records (tab) of data and allow the user to add additional tabs.

v-tab's can contain full form schema. This means you can build forms within forms!

!["Tabs Element"](/files/-LKI57mbKyEj8XrXfr2l)

| Key           | Type   | Description                                                                                                                |
| ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| tabLabelModel | string | Point this to the data model.field. It will be used to display on the tab.                                                 |
| min           | number | {optional} The minimum number of tabs the user is allowed to create. The delete icon will had when this number is reached. |
| max           | number | {optional} The maximum number of tabs the user is allowed to create. The add icons will hide when this number is reached.  |
| model         | array  | the data model must be initialized as an array. '***\[ ]'***                                                               |
| schema        | object | the schema object follows the same construct as schema of its parent. It only needs to contain a ***fields*** array.       |

### Notes

Set the min and max values to the same number to permanently hide the add and delete icons. Data within your data models array will still render rows accordingly.

```yaml
// typical v-tab object
// DATA MODE

{
  "hint": "You can set the 'max' property to limit number of records",
  "label": "Enter Some Contacts (4 max)",
  "min": 1,
  "max": 4,
  "model": "contacts",
  "schema": {
    "fields": [
      {
        "label": "First Name",
        "model": "nameFirst",
        "styleClasses": "col-md-6",
        "fieldOptions": {
          "inputType": "text"
        },
        "type": "input"
      },
      {
        "label": "Last Name",
        "model": "nameLast",
        "styleClasses": "col-md-6",
        "fieldOptions": {
          "inputType": "text"
        },
        "type": "input"
      },
      {
        "label": "Street 1",
        "model": "street1",
        "styleClasses": "col-md-12",
        "fieldOptions": {
          "inputType": "text"
        },
        "type": "input"
      }
    ]
  },
  "styleClasses": "col-md-8",
  "tabLabelModel": "nameFirst",
  "type": "vtabs"
}
```


# Panel

{% hint style="info" %}
To see panel in grouping eleelements, check out [this example](https://appstaging.fmbetterforms.com/#/apps/pages/edit?id=945D672C-B27E-2B4C-BEAA-54F5EC6F7435).
{% endhint %}

`panel` allows better organization of fields and data. Panel elements can hold other Klai elements.

|                 Key | Value(s) |    Type   | Description                                                                                   |
| ------------------: | -------- | :-------: | --------------------------------------------------------------------------------------------- |
|              `type` | panel    |  *string* |                                                                                               |
|             `model` |          |  *object* | unused                                                                                        |
|             `title` | *text*   |  *string* | Text string that will appear at the top of the panel                                          |
|            `footer` | *text*   |  *string* | Text that appears at the bottom of the footer.                                                |
|            `schema` | {}       |  *object* | Contains nested form schema                                                                   |
|     `schema.fields` | \[]      |  *array*  | Field elements to appear inside the panel                                                     |
|            `isOpen` |          | *boolean* | The current state of the panels display                                                       |
|       `credentials` | {}       |  *object* | credential object                                                                             |
|            `isForm` |          | *boolean* | Defines the panel as a form. [Learn more](/reference/form-settings/misc-page-settings#isform) |
| `panelStyleClasses` |          |  *string* |                                                                                               |
|  `bodyStyleClasses` |          |  *string* |                                                                                               |

### Reference

```yaml
// sample panel object
{
  "schema": {
    "fields": [
      {
        "label": "My Input",
        "model": "field1",
        "styleClasses": "col-md-12",
        "fieldOptions": {
          "inputType": "text"
        },
        "type": "input"
      }
    ]
  },
  "slots": [],
  "styleClasses": "col-md-6",
  "type": "panel"
}
```

###


# accordion

The accordion allows collapsable data rows, much like a portal.

[Accordion2 ](/reference/components-overview/grouping-elements/accordion-1)version adds slots that can replace the row header.

| Key           | Type   | Description                                                                                                                |
| ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| bfId          | object | a BetterForms UUID is generated and added to all browser side created records to help in post processing                   |
| defaultModel  | object | when provided will be used to populate browser side created rows.                                                          |
| min           | number | {optional} The minimum number of tabs the user is allowed to create. The delete icon will had when this number is reached. |
| max           | number | {optional} The maximum number of tabs the user is allowed to create. The add icons will hide when this number is reached.  |
| model         | array  | the data key the tab row data resides in. `model` must be initialized as an array. `[]`                                    |
| schema        | object | the schema object follows the same construct as schema of its parent. It only needs to contain a ***fields*** array.       |
| tabLabelModel | string | Point this to the data model.field. It will be used to display on the tab.                                                 |

## Notes

Set the min and max values to the same number to permanently hide the add and delete icons. Data within your data models array will still render rows accordingly.


# accordion2

The accordion allows collapsable data rows, much like a portal.

Accordion2 version adds slots that can replace the row header.

<table><thead><tr><th width="205">Key</th><th width="147.33333333333331">Type</th><th>Description</th></tr></thead><tbody><tr><td>bfId</td><td>object</td><td>a BetterForms UUID is generated and added to all browser side created records to help in post processing</td></tr><tr><td>defaultModel</td><td>object</td><td>when provided will be used to populate browser side created rows.</td></tr><tr><td>min</td><td>number</td><td>{optional} The minimum number of tabs the user is allowed to create. The delete icon will had when this number is reached.</td></tr><tr><td>max</td><td>number</td><td>{optional} The maximum number of tabs the user is allowed to create. The add icons will hide when this number is reached.</td></tr><tr><td>model</td><td>array</td><td>the data key the tab row data resides in. <code>model</code> must be initialized as an array. <code>[]</code></td></tr><tr><td>modelActiveTab</td><td>string</td><td>{optional} Model path for two-way binding of the active accordion index. When blank/undefined, all accordions are closed. Set to 0, 1, 2... to open a specific row. Updates when user toggles accordions.</td></tr><tr><td>schema</td><td>object</td><td>the schema object follows the same construct as schema of its parent. It only needs to contain a <em><strong>fields</strong></em> array.</td></tr><tr><td>slots</td><td>array</td><td>Array of slot objects. Slot names: <code>tabLabel</code><br><code>`row._index`</code> contains the current rows index</td></tr><tr><td>tabLabelModel</td><td>string</td><td>Point this to the data model.field. It will be used to display on the tab.</td></tr></tbody></table>

### Usage

```
{
                "label": "Enter Some Contacts (min. 1 max 4)",
                "max": 4,
                "min": 1,
                "model": "contacts",
                "slots": [{
                    "html": "This is row:{{row._index +1}}, with the full name: {{model.nameFirst}} {{model.nameLast}}`",
                    "slot": "tabLabel"
                }],
                "schema": {
                    "fields": [{
                        "label": "First Name",
                        "model": "nameFirst",
                        "styleClasses": "",
                        "fieldOptions": {
                            "inputType": "text"
                        },
                        "type": "input"
                    },{
                        "label": "Last Name",
                        "model": "nameLast",
                        "styleClasses": "",
                        "fieldOptions": {
                            "inputType": "text"
                        },
                        "type": "input"
                    }]
                },
                "styleClasses": "col-md-6",
                "type": "accordion2"
            }
```

### Controlling Which Accordion is Open (modelActiveTab)

> **Note:** Available from BetterForms v3.2.27+

Use `modelActiveTab` to control which accordion row is open. This creates two-way binding between the accordion state and your data model.

**Example:**

```json
{
  "type": "accordion2",
  "model": "contacts",
  "modelActiveTab": "activeContact",
  "slots": [{
    "html": "{{row.name}}",
    "slot": "tabLabel"
  }],
  "schema": {
    "fields": [{
      "type": "input",
      "label": "Name",
      "model": "name"
    }]
  }
}
```

**Model:**

```json
{
  "contacts": [
    { "name": "Ada Lovelace" },
    { "name": "Grace Hopper" }
  ],
  "activeContact": 1
}
```

In this example, the second accordion (Grace Hopper) opens on load because `activeContact` is set to `1`. When users toggle accordions, `activeContact` updates automatically. Leave it blank or undefined to start with all accordions closed.

## Notes

Set the min and max values to the same number to permanently hide the add and delete icons. Data within your data models array will still render rows accordingly.


# listrows

The `listrows` is used to display and manage a list of rows where each row consists of fields defined in the schema. This allows for the dynamic rendering of forms that include multiple rows of data inputs, such as address lists or contact details.

**Example Configuration**

{% hint style="info" %}
You can reference to [this example](https://examplesdev.fmbetterforms.com/#/form/4AE6421A-C3B9-4218-9DB8-7692A2486BD4).
{% endhint %}

The following example illustrates a `listrows` component where users can enter multiple contacts with specific fields for first name, last name, and address. It also demonstrates the flexibility of customizing the minimum and maximum number of entries.

```json
[
    {
        "isCollapsable": false,
        "schema": {
            "fields": [
                {
                    "label": "Enter Some Contacts (min. 2 max 4)",
                    "max": 4,
                    "min": 2,
                    "model": "address",
                    "schema": {
                        "fields": [
                            {
                                "label": "First Name",
                                "model": "nameFirst",
                                "styleClasses": "col-md-3",
                                "fieldOptions": {
                                    "inputType": "text"
                                },
                                "type": "input"
                            },
                            {
                                "label": "Last Name",
                                "model": "nameLast",
                                "styleClasses": "col-md-3",
                                "fieldOptions": {
                                    "inputType": "text"
                                },
                                "type": "input"
                            },
                            {
                                "label": "Street 1",
                                "model": "street1",
                                "styleClasses": "col-md-3",
                                "fieldOptions": {
                                    "inputType": "text"
                                },
                                "type": "input"
                            },
                            {
                                "label": "Street 2",
                                "model": "street2",
                                "styleClasses": "col-md-3",
                                "fieldOptions": {
                                    "inputType": "text"
                                },
                                "type": "input"
                            }
                        ]
                    },
                    "styleClasses": "col-md-8",
                    "type": "listrows"
                }
            ]
        }
    }
]
```

**Key Attributes**

* **`model`**: The data model path where the array of rows is stored. In this case, `address` is the model where each row of contact data is kept.
* **`min`**: Defines the minimum number of rows required (e.g., 2).
* **`max`**: Defines the maximum number of rows allowed (e.g., 4).
* **`schema.fields`**: An array of field configurations that will be displayed in each row. Each field can have its label, model, input type, and layout styling.

**Notes**

* The `listrows` component is versatile and can handle different types of input fields like text, checkboxes, and dropdowns.
* The display of Add and Delete icons is automatically handled based on the defined `min` and `max` values.


# Uploading Files


# dropzone

`dropzone` Allows you to easily add a drag and drop file upload component to any form. Files are posted to any upload service such as [file.io](https://www.file.io/#one) and [uploadcare](https://uploadcare.com). The data model is passed back all relevant meta information.

#### File.io

The default configuration uses `file.io` as ephemeral storage. Using additional options you can adjust the length of time a file lives for (default is 2 weeks). Typically upon form submission, your FileMaker hook script would insert the data from the storage location into a container etc in your database.

#### AWS S3

Files can be uploaded to an AWS S3 bucket. You will need to first provision the bucket. See the following page for complete setup instructions

{% content-ref url="/pages/umdMN8rlVNK5x8ugypw2" %}
[dropzone to S3](/reference/components-overview/uploading-files/dropzone-to-s3)
{% endcontent-ref %}

#### Base64

| Key             | Value(s)   | Type                  | Description                                                                                          |
| --------------- | ---------- | --------------------- | ---------------------------------------------------------------------------------------------------- |
| type            | `dropzone` | string                |                                                                                                      |
| model           |            | Array                 | Array of uploaded file metadata. Does not have to be predefined.                                     |
| useCustomSlot   | ()         | boolean               | If true, then the HTML in the `html` key will be inserted into the target drop area, defaults false  |
| includeB64      |            | boolean               | If true, base64 encoded data is returned in the response, defaults false.                            |
| options         |            | object                | Object of additional options. These will override defaults. and are passed directly into dropzone.js |
| html            |            | string                | Optional custom HTML rendered inside the drop area when `useCustomSlot` is true                      |
| awss3 / s3      |            | object                | Optional AWS S3 configuration passed through to the underlying vue-dropzone S3 integration           |
| s3UploadSuccess |            | function / `_actions` | Optional callback for successful S3 uploads                                                          |
| s3UploadError   |            | function / `_actions` | Optional callback for S3 upload errors                                                               |

## Minimal Usage Example

```yaml
{
  "label": "Upload File(s)",
  "model": "files",
  "styleClasses": "col-md-8",
  "type": "dropzone",
  "options": {
    // all additional items and overrides go here
  }
}
```

!["Dropzone Element"](/files/VnqFW593FUyKXLCyJqOm)

## Additional Options

You can totally customize the dropzone component and control things like file size limits and number of files uploaded.

BetterForms also exposes a few integration-specific additions on top of the standard Dropzone options:

* `awss3` or `s3` to enable the S3 upload mode
* `html` plus `useCustomSlot` to replace the default dropzone content
* `s3UploadSuccess` and `s3UploadError` hooks for S3-specific upload events

### Reference

[vue-dropzone options](https://github.com/rowanwins/vue-dropzone#props) <http://www.dropzonejs.com/#configuration>

## Example Data model

```yaml
[
  {
    "file": {
      "upload": {
        "progress": 100,
        "total": 67666,
        "bytesSent": 67666,
        "filename": "1404329470648.jpeg"
      },
      "status": "success",
      "previewElement": {},
      "previewTemplate": {},
      "_removeLink": {},
      "accepted": true,
      "processing": true,
      "xhr": {},
      "width": 450,
      "height": 385
    },
    "response": {
      "success": true,
      "key": "RSadAH",
      "link": "https://file.io/RSadAH",
      "expiry": "14 days"
    }
  }
]
```

### Reference

[vue-dropzone options](https://github.com/rowanwins/vue-dropzone#props)

<http://www.dropzonejs.com/#configuration>


# dropzone to S3

How to use the dropzone element to upload files directly to an AWS S3 bucket.

{% hint style="danger" %}
This page is still under development
{% endhint %}

## Introduction

If you want your users to upload large files, S3 is a great option. However, it can be tricky to navigate because of how it is secure by default. The Dropzone uploader wants to be able to push a file to a URL endpoint, but S3 doesn't provide that for security purposes. Other techniques suggest using a **Lambda** function to generate that signed URL, which is possible. However, this guide will focus on using the AWS JavaScript SDK to upload directly from a browser.

{% hint style="warning" %}
The method that will be outlined on this page is **advanced** and not directly supported by BetterForms. These techniques were compiled from around the internet and are implemented into BetterForms by taking advantage of how BetterForms exposes HTML and JavaScript in the page editor
{% endhint %}

## Configuring AWS

If you don't already have an AWS account, sign up [here](https://portal.aws.amazon.com/billing/signup#/start).

### 1. Amazon Cognito

Amazon Cognito is the user management service that we'll use to authenticate our application to AWS for uploading. Cognito allows for us to configure permissions for anonymous users, which will be useful in this case so that we don't have to worry about user credentials when uploading to our bucket.

1. From the AWS Console, navigate to the **Cognito** service.
2. Make sure you're in your preferred region (Cognito is not available in all regions)
3. Create a new Identity Pool
4. Be sure to check the box labeled **Enable access to unauthenticated identities**. All other settings can be left at default, then click **Create Pool**
5. The next page will create new roles in IAM for this identity pool. Simply click **Allow** to continue.
6. In the final confirmation screen, make note of your **Identity Pool ID**.

### 2. Configuring your S3 bucket

Let's create a new bucket for you uploads. Since this bucket will have public access, it's good to make a separate bucket so that you can isolate data concerns.

1. From the AWS Console, navigate to the **S3** service.
2. Create a new bucket. Make sure that it's configured in the same region as your Cognito Identity Pool that was setup in step 1.
3. When configuring the bucket, all settings can be left at their defaults.
4. Navigate to the bucket, then to the **Permissions** tab.
5. In the **CORS configuration** section, add the following code. This will allow any browser to send a PUT request to your bucket, which is how the upload works

```markup
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "POST",
            "GET"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag"
        ],
        "MaxAgeSeconds": 3000
    }
]
```

### 3. Configuring IAM policy

Lastly, we need to configure the policy for Cognito Role that we created in step 1 to have write access to the bucket we created in step 2.

1. From the AWS Console, navigate to the **IAM** service.
2. In the sidebar, click on **Roles** and locate the Unauth role that was created for your Cognito Identity Pool. It should follow a pattern like `Cognito_appnameUnauth_Role` where `appname` is the name you gave to your Identity Pool.
3. Choose the option to **Add inline policy**, then navigate to the JSON tab and paste in the following code. These are the absolute minimal settings that will allow this anonymous user to put things into the bucket but nothing else.

```yaml
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucketMultipartUploads",
                "s3:ListMultipartUploadParts",
                "s3:PutObject",
                "s3:GetObjectAcl",
                "s3:AbortMultipartUpload",
                "s3:GetBucketCORS",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::example-bucket/*",
                "arn:aws:s3:::example-bucket"
            ]
        }
    ]
}
```

{% hint style="warning" %}
Make sure you change the `example-bucket` in lines 17-18 with the name of **your own** bucket
{% endhint %}

Now we're ready to go back to BetterForms! Feel free to navigate back to S3 and keep it open in a new tab for inspection of your files as we upload them...

## Configuring your Site Settings

Since this method requires external libraries not provided by BetterForms, you'll need to inject them into your site using a [DOM Header Insertion](/reference/site-settings/slots-code-injection#dom-header-insertion).

```markup
<script src="https://cdnjs.cloudflare.com/ajax/libs/aws-sdk/2.543.0/aws-sdk.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.2.0/min/dropzone.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.2.0/min/dropzone.min.css" rel="stylesheet">
```

{% hint style="info" %}
These libraries may have been updated since the time of this writing, check the original sources for the most updated CDN links.

* [AWS SDK](https://cdnjs.com/libraries/aws-sdk)
* [Dropzone.JS](https://cdnjs.com/libraries/dropzone)
  {% endhint %}

## Configuring your Page

Now that you have your AWS bucket and credentials setup, you can plug in your values to the [example page](https://app.fmbetterforms.com/#/forms/formedetail?id=4994183D-C0B6-0444-A443-032904BDA204) to see this in action. The rest of this guide assumes that you have duplicated that page into your own site so that you have access to the JavaScript and HTML code there.

### HTML

To insert the Dropzone element in your page, we'll use an [HTML element](/reference/components-overview/common/html). The id of `myDZ` is the most crucial piece, and is how we will target this element using the JavaScript.

```markup
<form id="myDZ" action="/file-upload" class="vue-dropzone dropzone">
  <div class="fallback">
    <input name="file" type="file" multiple />
  </div>
  <div class="dz-message text-center">
      <!-- Customize this div with your own elements -->
      <h1>Drop your file here, or click to browse</h1>
  </div>
</form>
```

### JavaScript

To initialize the dropzone, you need to run some JavaScript code that programmatically configures all of the settings. I recommend doing this in a [named action](/reference/actions-processor/actions_named#defining-named-actions) on the page so that you can call it at a specific place in your users workflow if necessary. In my testing, I found that putting this code in the `onFormLoad` named action works about half of the time because sometimes the code runs before the page is fully rendered and therefore cannot be attached to the form.

{% hint style="info" %}
See the full code in the `initDZ` named action on the example page.
{% endhint %}

The first few lines of this code is where you'll configure with your settings from AWS. Feel free to hard-code these value into the JavaScript function instead of referencing model unless you want to programmatically change them later.

Your **bucketRegion** should match the start of your **Identity Pool ID**. For example, if your Identity Pool ID is `us-west-2:5fxxxxxxx-2696-4xxxx-8xxxxxx-dxxxxxxxx` then your bucket region would be `us-west-2`

```javascript
var bucketName = model.settings.bucketName;
// var bucketName = 'example-bucket';   <-- for hard coding, use quotes
var bucketRegion = model.settings.region;
var IdentityPoolId = model.settings.identityPool;
```

For the **options** variable, see the [Dropzone documentation](https://www.dropzonejs.com/#configuration) if you'd like to further customize the element.

If you follow along with the code, you'll notice that I'm adding files to an array as they are added to the dropzone and moving them to a *different* array when the upload is complete. Finally, the named action called `onDropzoneComplete` runs when all the files have uploaded. This is where you can run a [Utility Hook](/reference/actions-processor/actions_overview/runutilityhook) back to your FMS to save the data in your database.

{% hint style="warning" %}
Be sure to leave the default keys in the Data Model when duplicating the example page. These keys are directly referenced by the JavaScript code and it may fail if these keys do not exist.
{% endhint %}

### Manipulating the Queue

Since we are adding this element completely from scratch, we have to do a little bit more work to interact with the dropzone. It's **not** integrated into Vue like the rest of the elements you may be used to in BetterForms, so we have to use regular JavaScript functions to work with it.

#### The following 3 lines of code is how the Dropzone is initialized.

```javascript
// bind the events to the Dropzone so that Dropzone will callback our functions
// when it's done with certain tasks (see Dropzone Events docs)
Dropzone.prototype.uploadFiles = files => files.map(sendEvents);

// initilaize the dropzone based on the id 'myDZ' and options provided
var dz = new Dropzone('#myDZ', options)

// store the dropzone object at the window level so it
// can be referenced globally by other actions
window.dz = dz
```

If you keep reading the code in the demo file, you'll see several `dz.on(...)` functions. These functions are the [events](https://www.dropzonejs.com/#events) called when things happen in the dropzone element. One example is the `addedfile` event, which is called when a file is added to the dropzone:

```javascript
dz.on("addedfile", function(file) {
    formSchema.model.dropzone.queue.push(file.upload.uuid)
});
```

This simple function pushes the `uuid` key of the file that is passed into this function to an array so that you can better work with the queue using Vue.

{% hint style="success" %}
In production, it's better to pass the entire `file` object into an array so that you can get at more details of the file object instead of just the uuid. The reason it's setup like this for the demo is because the entire file object cannot be printed on screen as code. Once you fully understand how to manipulate the queue, be sure to update **all** references to this array (it's also used in the `removedFile` event, for example)
{% endhint %}


# uploadCare

`uploadCare` gives you file uploads, media processing, and adaptive delivery for web and mobile by providing a widget allowing multiple file uploads from various sources including social media.

See UploadCare for more detailed information <https://uploadcare.com/>

The component is based on UploadCares JS widget.\
**Documentation**: <https://uploadcare.com/docs/api_reference/javascript/>

**Version**: > 0.8.18

## Usage

1. Register for a free account at [www.uploadcare.com](https://www.uploadcare.com/)
2. Add your API public key into the schema
3. Uploaded file data is found in the `model` key.

## Key Properties

| Key                        | Value(s)     | Type   | Description                                                                       |
| -------------------------- | ------------ | ------ | --------------------------------------------------------------------------------- |
| `type`                     | `uploadcare` | string | Identifies the Uploadcare element                                                 |
| `model`                    |              | string | Data model key that will receive the uploaded file info array                     |
| `options`                  | `{}`         | object | Options passed directly into the Uploadcare widget                                |
| `globals`                  | `{}`         | object | Global Uploadcare settings written onto `window` before the widget is initialized |
| `onUploadComplete_actions` | `[]`         | array  | Actions to run after the upload completes                                         |

### Notes:

Not all of UploadCare's options setting ar available as `options.someOption` keys, some are only available as global variables. To handle this BetterForms adds provision for adding both `GLOBAL` and `JS` options. See options documentation for what ones are supported under what key.

Fully read the UploadCare documentation before posting support requests.

`onUploadComplete_actions` is the user-facing schema key. BetterForms translates it into the runtime `onUploadComplete` callback internally.

```
// Example
{
    "globals": {
        "UPLOADCARE_CLEARABLE": true,
        "UPLOADCARE_LOCALE_TRANSLATIONS": {
            "buttons": {
                "choose": {
                    "files": {
                        "other": "Add Files"
                    }
                }
            }
        }
    },
    "model": "newFiles",
    "onUploadComplete_actions": [{
        "action": "runUtilityHook",
        "options": {
            "type": "add"
        }
    }],
    "options": {
        "crop": true,
        "multiple": true,
        "publicKey": "922fff6657dc1ae880be"
    },
    "styleClasses": "col-md-2",
    "type": "uploadcare"
}
```


# Uppy File Upload Widget Integration with AWS S3

{% hint style="warning" %}
This page is in development
{% endhint %}

This page provides instructions on how to integrate the Uppy file upload widget with AWS S3 for seamless file uploads. It also includes helper functions for path extraction and named actions.

#### **1. Uppy Library**

Include the Uppy library and its CSS in your DOM Header Insertions.

```html
<script src="https://releases.transloadit.com/uppy/v3.7.0/uppy.min.js"></script>
<link href="https://releases.transloadit.com/uppy/v3.7.0/uppy.min.css" rel="stylesheet">
```

#### 2. Path Extraction Function

Include the the `BF.getPaths` in your DOM Header Insertion. This function is used to extract JSON paths from a data object based on a filter. This can be helpful for dynamically generating paths to specific data elements.

```html
<script>
    BF.getPaths = function(data, filter) {
        var paths = jp.paths(data, filter);
        var arrayLength = paths.length;
        var lineResult = [];
        for (var i = 0; i < arrayLength; i++) {
            var line = paths[i];
            for (var j = 0; j < line.length; j++) {
                var term = line[j];
                if (term == "$") {
                    // Root element
                } else if (typeof term === "number") {
                    lineResult[i] = lineResult[i] + "[" + term + "]";
                } else {
                    lineResult[i] = lineResult[i] == null ? "data." + term : lineResult[i] + "." + term;
                }
            }
        }
        return lineResult;
    }
</script>
```

#### 3. Named Action Promise Function

Include the `BF.namedActionPromise` function in your DOM Header Insertion. It facilitates the execution of named actions with a promise-based approach. This function generates a unique ID for each action and resolves the promise once the action is complete.

```html
<script>
    BF.namedActionPromise = function(namedActionName, options) {
        var idResolver_uid = BF.getUUID();
        options.idResolver_uid = idResolver_uid;
        return new Promise((resolve, reject) => {
            window.namedActionResolve[options.idResolver_uid] = resolve;
            BF.namedAction(namedActionName, options);
        });
    };
</script>
```

#### **4. AWS S3 Setup**

Ensure you have an AWS S3 bucket and the necessary credentials for accessing it. You can reference [here](/reference/components-overview/uploading-files/dropzone-to-s3).

{% hint style="info" %}
Check [this example](https://app.fmbetterforms.com/#/apps/pages/edit?id=FR_A1E5B73B-3238-1A43-A212-92999C2EA9CF) for Uppy used in various cases.
{% endhint %}

#### Action Scripts

`getSignedURL` handles the process of obtaining a signed URL from AWS S3 to upload files securely.

`resolver` resolves the signed URLs obtained for file uploads.

`runSignedURL` triggers the process to get the signed URL for file uploads.

`uploadHandler` handles the post-upload process.

`onFormLoad` named action: this initializes the necessary variables and Uppy instances for different upload scenarios, including drag-drop area, status bar, dashboard trigger, and file input.

```javascript
// Initialize session and promise resolver
window.namedActionResolve = {};
model.idSession = BF.getUUID();

// Initialize Uppy instances with configurations
window.uppy1 = new Uppy.Uppy({ /* ... */ });
window.uppy2 = new Uppy.Uppy({ /* ... */ });
window.uppy3 = new Uppy.Uppy({ /* ... */ });
window.uppy4 = new Uppy.Uppy({ /* ... */ });

// Additional setup and event listeners for Uppy instances
```


# Misc Elements


# Plain Text / Code Editor

The `aceeditor` element is great for editing HTML, CSS, JSON, or any other type of code in your app. This is the same element that is used by throughout the BetterForms interface.

|           Key |   Value(s)  |            Type           | Description                                                                                                                                                 |
| ------------: | :---------: | :-----------------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|        `type` | `aceeditor` |          *string*         |                                                                                                                                                             |
|        `lang` |             |          *string*         | code language options: html, javascript, markdown, plain\_text, css, json                                                                                   |
|       `model` |             | *string, object or array* | the field or JSON node of the model used for the source                                                                                                     |
|   `getAsJSON` |  *optional* |         *boolean*         | if true, the model is stringified for editing. This allows you to edit the JSON directly in the MODEL (vs it being stored as a string (all other languages) |
|      `height` |  *optional* |          *string*         | default of 700px else element height can be specified `300px`, `70%` are valid values.                                                                      |
|       `theme` |  *optional* |          *string*         | The theme used in the editor                                                                                                                                |
|    `readOnly` |  *optional* |         *boolean*         | If true, the editor content cannot be edited.                                                                                                               |
|   `foldDepth` |  *optional* |          *number*         | Applies code folding when the editor first loads.                                                                                                           |
| `gutterclick` |  *optional* |         *function*        | Callback fired when the gutter is clicked and the current line can be parsed as a JSON key/value pair.                                                      |

Notes:

* Supported languages in the component include `html`, `javascript`, `markdown`, `plain_text`, `css`, and `json`.
* Built-in themes loaded by BetterForms include `chrome`, `solarized_light`, and `dracula`.
* When `getAsJSON` is `true`, the editor reads and writes the model as parsed JSON rather than a plain string.

## Example

```yaml
// Sample JSON editor ( will put stringified json text into the model field )
{
  "getAsJSON": false,
  "height": "500px",
  "label": "EDIT Some JSON",
  "lang": "json",
  "model": "sourceCode",
  "styleClasses": "col-md-12",
  "type": "aceeditor"
}
```


# signature

The `signature` element is a basic signature capture that is mobile compatible. The captured signature is converted to base 64 and added to the data model.

```yaml
// Signature capture pad
{
    "height": 150,
    "hint": "By signing this form you agree to the terms and conditions as stated somewhere else.",
    "label": "Sign your name ...",
    "model": "signature",
    "required": true,
    "styleClasses": "col-md-6",
    "type": "Signature",
    "validator": "string"
}
```

## Runtime Notes

* The saved value is a data URL string from the canvas, or an empty string when the pad is cleared.
* A built-in **Clear** button is rendered below the signature pad.
* `height` controls the canvas height; the width follows the container width.


# fullCalendar

Implementation of www\.fullcalendar.io calendaring component. Component based on https\://github.com/CroudTech/vue-fullcalendar

Implementation of [www.fullcalendar.io](https://fullcalendar.io/) calendaring component.

Component based on <https://github.com/CroudTech/vue-fullcalendar>

## Runtime Shape

The live component:

* reads events from the field `model`
* passes those events into the calendar as the field value
* merges `schema.config` over a default config where `defaultView` starts as `month`

## Common Schema Keys

| Key      | Type     | Description                                                        |
| -------- | -------- | ------------------------------------------------------------------ |
| `type`   | `string` | Must be `fullCalendar`                                             |
| `model`  | `string` | Model path containing the calendar event array                     |
| `config` | `object` | Calendar configuration object passed into the underlying component |

Example:

```json
{
  "type": "fullCalendar",
  "model": "events",
  "config": {
    "defaultView": "month",
    "editable": true
  }
}
```

## Callback Keys

The current implementation wires these schema callback functions:

* `eventSelected(...args)`
* `eventDrop(...args)`
* `eventResize(...args)`
* `eventCreated(...args)`
* `eventRender(...args)`
* `eventMouseover(...args)`
* `eventMouseout(...args)`
* `dayClick(...args)`

These are JavaScript callback functions on the field schema, not `*_actions` arrays.

## Notes

* If you do not provide `config.defaultView`, the component defaults to `month`.
* The component passes the full field schema through to the underlying calendar component with `v-bind=\"schema\"`.
* The published docs previously referenced `*_actions` keys, but the live component code currently uses direct schema callback functions instead.


# rangeSlider

The live `rangeSlider` field uses the `ion.rangeSlider` jQuery plugin.

## Source Reference

Vue-form-generator: <https://vue-generators.gitbook.io/vue-generators/fields/optional_fields/slider>

ion.rangeSlider.js: <http://ionden.com/a/plugins/ion.rangeSlider/index.html>

!["Range Slider Element"](/files/-LaB-T3w6321JZGwHMUf)

Add the CDN references below into the DOM Header Insertions section of your site.

```
<!--rangeSlider CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.0/css/ion.rangeSlider.min.css"/>

<!--Plugin JavaScript file-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.0/js/ion.rangeSlider.min.js"></script>
```

## Common Configuration Properties

| Property       | Type      | Description                                                      |
| -------------- | --------- | ---------------------------------------------------------------- |
| `type`         | `String`  | Must be `"rangeSlider"`                                          |
| `label`        | `String`  | Field label                                                      |
| `model`        | `String`  | Model key that stores either a single number or a two-item array |
| `placeholder`  | `String`  | Placeholder text                                                 |
| `disabled`     | `Boolean` | Disables the control                                             |
| `readonly`     | `Boolean` | Makes the input read-only                                        |
| `fieldOptions` | `Object`  | Options passed directly into `$(el).ionRangeSlider(...)`         |

## Example Schema Snippet

```
{
  "label": "Rank",
  "styleClasses": "col-md-12 ",
  "model": "rank",
  "fieldOptions": {
    "min": 1,
    "max": 10,
    "grid": true
  },
  "type": "rangeSlider"
}
```

## Runtime Notes

* The component requires the `ion.rangeSlider` library to be loaded globally.
* `fieldOptions.type` defaults to `"single"` if you do not provide it.
* When the slider type is `"double"`, the model is written as `[from, to]`.
* Otherwise, the model is written as a single numeric value.

### Theming

see docs for theming info here: <http://ionden.com/a/plugins/ion.rangeSlider/skins.html>

Use key `"skin"` to change theme.

```
"fieldOptions": {
    "grid": true,
    "skin": "big"
  }
```


# Payment Gateways


# Authorize.net

`authorize` Allows you to securely process payments via Authorize.net This component only renders the button and hands the secure transaction via Authorize.net's accept.js module. The workflow for this component requires a bit more work than the Stripe or PayPal payment gateways. Unlike those elements, `authorize` requires server side calls the the Authorize.net API to complete the transaction.

**Version:** >0.8.16

{% hint style="danger" %}

#### This component documentation is work in progress

{% endhint %}

See <https://developer.authorize.net/api/reference/features/acceptjs.html> for workflow reference and deeper understanding of this component.

When the rendered button is clicked, a modal window is opened that will contain card capture information. This window is an iFrame and generated securely via the accept.js model. Once the payment information has been entered,

## Credentials

You will need to obtain credentials from the Authorize.net dashboard. The BetterForms component uses the public `apiLoginID` and `clientKey` values.

If you need to locate the public client key in Authorize.net, the dashboard screen looks like this:

<figure><img src="/files/-LWBdGQEI9LoWckgt5XA" alt="Authorize.net public client key screen"><figcaption></figcaption></figure>

| Key                     | Value(s)         | Type    | Description                                                               |
| ----------------------- | ---------------- | ------- | ------------------------------------------------------------------------- |
| `type`                  | authorize        | string  | Identifies the Authorize.net element                                      |
| `model`                 |                  | string  | Data model key that will contain the response returned from Authorize.net |
| `apiLoginID`            |                  | string  | Public API Login ID from Authorize.net                                    |
| `clientKey`             |                  | string  | Public client key used by AcceptUI                                        |
| `buttonText`            |                  | string  | Text displayed on the rendered button                                     |
| `buttonClasses`         |                  | string  | CSS classes applied to the button                                         |
| `acceptUIFormBtnTxt`    |                  | string  | Text shown on the AcceptUI submit button                                  |
| `acceptUIFormHeaderTxt` |                  | string  | Header text shown in the AcceptUI dialog                                  |
| `billingAddressOptions` | `{}`             | object  | Billing-address settings passed to AcceptUI                               |
| `sandBox`               | `true` / `false` | boolean | If true, BetterForms loads the Authorize.net sandbox AcceptUI script      |
| `onResponse_actions`    | `[]`             | array   | Actions to run after AcceptUI returns a response                          |

```javascript
// Sample schema object
{
  "model": "authorize",
  "styleClasses": "col-md-4",
  "type": "authorize",
  "acceptUIFormBtnTxt": "Complete Payment",
  "acceptUIFormHeaderTxt": "Card Payment Information",
  "apiLoginID": "9vR4pUBE483",  
  "billingAddressOptions": {
    "required": true,
    "show": true
  },
  "buttonClasses": "btn btn-lg btn-primary",
  "buttonText": "Pay Now",
  "clientKey": "7amDN97QG6xkHGB4xdD2yd3BYvdZ49HFjp7j7477U282BYuwuheDMvk4zE2R44b8",
  "sandBox": true,
  "onResponse_actions": [{
      "action": "function",
      "function": "console.log('@onResponse: ', action.options.args[0])"
  }]
}
```

## Response Hook

After AcceptUI returns a response, BetterForms stores that response in the configured `model` key and runs `onResponse_actions` if you configured them.

This is **not** an automatic utility-hook flow by itself. If you want to send the payment token or opaque data to FileMaker, add a `runUtilityHook` action inside `onResponse_actions`.

For example:

```json
{
  "onResponse_actions": [{
    "action": "runUtilityHook",
    "options": {
      "type": "authorize"
    }
  }]
}
```

In practice, the response object contains values such as:

```json
{
  "messages": {
    "resultCode": "Ok"
  },
  "opaqueData": {
    "dataDescriptor": "COMMON.ACCEPT.INAPP.PAYMENT",
    "dataValue": "..."
  },
  "encryptedCardData": {
    "cardNumber": "XXXXXXXXXXXX4242",
    "expDate": "02/21"
  }
}
```


# PayPal

`paypal` Allows you to securely process payments via PayPal. This component only renders the button and hands the secure transaction via the PayPal API. You will have to add summary information around this component so the user knows what they are paying for.

This element in an implementation of [vue-paypal-checkout](https://github.com/khoanguyen96/vue-paypal-checkout)

!["PayPal Element"](/files/-LKI5Ad4IjGXLcfUtA_n)

This element allows you to take payments via PayPal.

When clicked, the payment workflow is initiated.

| Key                    | Value(s)                  | Type   | Description                                                                                                                                                 |
| ---------------------- | ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| type                   | paypal                    | string |                                                                                                                                                             |
| model                  |                           | string | Data model key that will contain the PayPal response                                                                                                        |
| amountKey              |                           | string | model key that holds the amount.                                                                                                                            |
| onPaypalEvent\_actions | \[]                       | array  | If supplied, these actions run for PayPal events. If not supplied, the component falls back to the default `runUtilityHook` / `onUtility` server-hook flow. |
| currency               |                           | string | Currency for the payment, for example `USD` or `CAD`                                                                                                        |
| invoiceNumber          |                           | string | Optional invoice number passed to PayPal                                                                                                                    |
| env                    | `sandbox` or `production` | string | PayPal environment to use                                                                                                                                   |
| itemsKey               | 'myItems'                 | array  | optional - model key that holds an array of items                                                                                                           |
| credentials            | {}                        | object | credential object,                                                                                                                                          |
| style                  | {}                        | object | PayPal defined styling of the button                                                                                                                        |

### Reference

```javascript
// sample credential object
credentials: {
    sandbox: '<sandbox client id>',
    production: '<production client id>'
}
```

### Specifying Items

Optionally, according to the PayPal Payments API documents, you can list out any items along with your transaction.

For more information, PayPal Item List

```yaml
// Sample Items Object
myItems: [
    {
      "name": "hat",
      "description": "Brown hat.",
      "quantity": "1",
      "price": "5",
      "currency": "USD"
      },
      {
      "name": "handbag",
      "description": "Black handbag.",
      "quantity": "1",
      "price": "5",
      "currency": "USD"
      }
  ]
```

### Button Style

You can change the style of the button via a style object like so:

```javascript
{
    label: 'checkout',
    size:  'responsive',    // small | medium | large | responsive
    shape: 'pill',         // pill | rect
    color: 'gold'         // gold | blue | silver | black
}
```

### Complete Example Object

```yaml
{
  "label": "Paypal",
  "model": "files",
  "styleClasses": "col-md-8",
  "type": "paypal",
  "currency": "CAD",
  "locale": "ca",
  "env": "sandbox",
  "amountKey": "paymentAmount",
  "onPaypalEvent_actions": [{
    "action": "runUtilityHook",
    "options": {
      "type": "paypal"
    }
  }],
  "credentials": {
    "sandbox": "AfGtki3XCbYBRxGWWY6YQlqRio82v5Jp6oPC7FJ9_0BLOlT3Z5KXLgPVmVGoCtZQTDuaYhrCM7ez3P9g",
    "production": "AcWg3pjwEtxVCX_UNrmb8GDvh3ntnP7zeDbIfVKRvDVWHr2UkkYUM9ze0r4-H4HkhzGtBEXE21iFsmg2"
  },
  "style": {
    "label": "checkout",
    "size": "responsive",
    "shape": "pill",
    "color": "gold"
  }
}
```

## Notes

* The runtime reads the amount from `model[amountKey]`, so use `amountKey` rather than a literal `amount` property on the element.
* `onPaypalEvent_actions` is the user-facing schema key. BetterForms translates it into the runtime callback function internally.


# Stripe

The `Stripe` component allows you to securely process payments via Stripe. This component only renders the button and handles the secure transaction via the Stripe API. You will need to add summary information around this component so the user knows what they are paying for.

This element is an implementation of the Stripe Checkout integration.

When clicked, the payment workflow is initiated.

#### Key Properties

| Key                | Value(s) | Type   | Description                                                                            |
| ------------------ | -------- | ------ | -------------------------------------------------------------------------------------- |
| type               | stripe   | string | Identifies the type of payment processor being used.                                   |
| model              |          | string | Data model key that will contain the Stripe response.                                  |
| product            |          | object | Contains details about the product being purchased.                                    |
| onSuccess\_actions |          | array  | Actions to run when the payment is successfully processed.                             |
| button             |          | string | Text to be displayed on the payment button.                                            |
| buttonClass        |          | string | CSS classes to style the button.                                                       |
| formId             |          | string | The ID of the form element containing the button.                                      |
| apiKey             |          | string | Your Stripe API key. Use your test key in development and your live key in production. |
| styleClasses       |          | string | CSS classes to style the component.                                                    |
| options            |          | object | Additional configuration options for the Stripe integration.                           |

#### Example Usage

```
{
  "options": {},
  "apiKey": "pk_test_goJPTw3PtFPLN6F0pD9GMUvI",
  "button": "Buy Now",
  "onSuccess_actions": [],
  "buttonClass": "btn btn-primary",
  "formId": "myButton1",
  "model": "stripeResponse",
  "product": {
    "amount": 100,
    "description": "562 pages hard cover book.",
    "name": "Acme Corporation"
  },
  "styleClasses": "col-lg-3",
  "type": "stripe"
}
```

## Notes

* `onSuccess_actions` is the user-facing schema key. BetterForms translates it into the runtime `onSuccess` callback internally.
* The component passes your `product` and `options` objects through to the Stripe Checkout integration.


# Adding Custom Page Elements

Third party modules can be incorporated into BetterForms apps. This opens up en enormous library of open source and paid modules. You should have a good working knowledge of VueJS and how binding works.

The easiest modules to incorporate into BetterForms are Vue based. Most Vue Modules start with `v-someName`

Currently modules will need a CDN link and must install themselves. (See limitations below)

## Example: V-Calendar

We will add `v-calendar` a beautiful date picker.

![V-Calendar](/files/-LYA8KhqYl-JXa_pH62u)

To reference the source code we will add the CDN link by adding the following to the DOM Header insertions for you site. The first line adds the CSS style sheet, the second is for the module code.

```markup
<link rel='stylesheet' href='https://unpkg.com/v-calendar/lib/v-calendar.min.css'>
<script src='https://unpkg.com/v-calendar@2.1.5/lib/v-calendar.umd.min.js'></script>
```

This module installs itself and will be available in `HTML` elements.

In the documentation for this library (<https://vcalendar.io/>) we see:

```javascript
<v-calendar :attributes='attrs'>
</v-calendar>
```

Here the `:attributes` key is bound to a variable called `attrs` To make the BetterForms compatible, we need to bind the `atributes` key to a BetterForms data source. We will use `model` like this: *(remember: an attribute must have a colon in front of it to be compatible with Vue bindings)*

```
<v-date-picker 
    :available-dates='model.availableDates' 
    v-model='model.selectedDates'
    >
</v-date-picker>
```

The `v-model` in the component points to the data source used to populate the calendar.

This code is inserted into an HTML element and we are done!

## Limitations

There are some limitations when adding 3rd party libraries.

* Module libraries must be available via a CDN
* Modules must self install as a global Vue component
* Modules must not require initialization code and be ready for use as a Vue component
* Modules that are not Vue components can still be used but will often require writing small JS interface code to help patch them into the Klai framework.
* Be sure to make not if the type of links as some are cached much slower than others, see: <https://forum.fmbetterforms.com/t/cdn-advice-if-your-site-is-slow-to-load/239>




---

[Next Page](/llms-full.txt/1)

