Custom Components

Components are chunks of reusable code. They allow you to define code in a single place and reuse it throughout your app. Components can be references through HTML or with BF JSON schema

Building and Customizing

Both the BF JSON and the HTML APIs are the same. Keys within the BF JSON schema can be referenced within a component as schema.<key> (for example, schema.title).

Component API

Key
Type
Description

fields

array

Optional either html or fields keys must be present. Contains BF schema elements normally found in the fields key of the page editor. The schema will be used as a component. If supplied will act as the source of the field schema and replace the components default fields if there are any.

schema

object

All schema keys can be accessed from within the component as schema.myTitle This allows you to pass data dynamically into the component

html

string

Contains HTML to be rendered as a component.

<bfcomp>

HTML tag name needed to identify a BF component

name

string

The component name

modelSource

object

used to supply the model will be for this component, in not supplied, the current model for the element is used.

modelDev

object

Optional. Used to add model keys that can be used for development and tests of components rendered where there is no model available. ( Used by the BF editor )

source

{ component }

If supplied, this will act as the entire source for the component. This attribute was added to allow the BF component editor to preview live components. You probably will never use this key. component is a complete component object (not documented in this doc, see support if needed)

attributes

various

All additional attributes supplied will override the component's schema keys where applicable. You can add any additional attribute or 'props' you need. Eg: buttonLabel

Usage as an JSON Schema element

{
    "name": "MyComponentName",
    "styleClasses": "",
    "type": "bfcomponent"
}

Usage as an HTML Vue component

Inline example: calling a component-scoped action

Context

Components mostly act the same as any other BF element for context. They see model and app the same regardless of HTML or BF JSON element schema.

Component-Scoped Named Actions

This note focuses on component-internal named actions: defining actions on components, how resolution works inside bfcomponent templates, and the onMount lifecycle behavior (v3.2.18+ Beta).

Prerequisite: Custom Components as Schema or HTML/Vue

See usage patterns and <bfcomp> embedding below: Usage as an HTML Vue component


Named actions overview

For a full introduction to named actions (definition, execution contexts, and general examples), see:

Named Actions (Action Scripts)

Component‑internal named actions and lifecycle (v3.3.3+)

This section documents how named actions defined on custom components are resolved and how lifecycle hooks work.

Where to define component named actions

You can define named actions directly on the component definition. Both locations below are supported when resolving from a component template:

or nested under comp:

How resolution works inside component templates

When a template rendered inside a bfcomponent calls namedAction('some_name'), the runtime resolves the chain in this order:

    1. form.namedActions[some_name] (form‑level overrides)

    1. Component definitions in site content and site root:

    • component.namedActions[some_name]

    • component.comp.namedActions[some_name]

This means a form can intentionally override a component's action by using the same name at form scope.

Lifecycle Hooks (v3.3.3+)

BetterForms components now support full lifecycle hooks that fire at specific points in the component's lifecycle. All lifecycle actions properly await async operations before proceeding.

Available Lifecycle Hooks

Hook
When It Fires
Blocks Rendering?
Use Case

onBeforeMount

Before component renders

βœ… YES

Load external libraries, initialize before render

onMount

After component in DOM

❌ No

Post-render logic, analytics, DOM operations

onUpdated

After component re-renders

❌ No

React to data changes, update third-party widgets

onBeforeDestroy

Before component destroys

❌ No

Cleanup: remove listeners, clear timers

onDestroyed

After component destroyed

❌ No

Final logging, analytics

Naming Support: Both onMount / mounted, onBeforeMount / beforeMount naming conventions are supported.

Critical Feature: onBeforeMount Blocks Rendering

The onBeforeMount hook is special - it blocks the component from rendering until all async actions complete. This solves the common problem of components rendering before external libraries load.

Example: Loading ApexCharts

Result: Component waits for libraries to load, then renders. No "undefined component" errors!

How onBeforeMount Works

  1. Component created (Vue lifecycle)

  2. beforeMount() hook fires

  3. Dispatches onBeforeMount actions

  4. Component waits for Promise to resolve

  5. Actions complete (e.g., library loads)

  6. Component renders βœ…

Using Other Lifecycle Hooks

onMount - Post-render logic

onBeforeDestroy - Cleanup

onUpdated - React to changes

Notes

  • Lifecycle hooks fire once per component instance

  • Multiple instances on the same page each run their own lifecycle actions

  • onBeforeMount is the only hook that blocks (by design)

  • Other hooks fire asynchronously but still await completion

  • Prefer making hooks idempotent when possible


Best practices

  • Naming to avoid collisions: Prefix internal named actions with a short component prefix to reduce collisions with form/global actions, e.g., uppy_fileUpload, chat_sendMessage.

  • Keep names stable: If your component is referenced externally (schema or tools), treat internal action names as API.

  • Propagate options: Make sure options passed at call sites flow to the chain; avoid overwriting action.options in custom function steps.

Single Uppy instance (global)

Uppy should be initialized once globally. Guard at the start of onMount so subsequent component instances no‑op.

Put a single function action first in onMount and short‑circuit if already initialized.

Choose a stable key (k) per feature you initialize.

Make the rest of onMount idempotent

  • Check for existing global objects or listeners before creating new ones (e.g., if (window.uppy) return;).

  • Avoid attaching duplicate event handlers; if needed, track a flag (window.__bfHandlers.uploaderReady = true).

Example: Single‑instance Uppy (JS)


Instance‑specific arguments pattern (recap)

When a component instance needs to pass per‑instance values (like modelPath, accept, or apiKey) into a named action, pass them directly via the call from the template. The options object propagates through the chain.

Example (inside component HTML):

In your action step, read them from action.options:

This avoids per‑instance registration in the global store while keeping actions reusable.


Gotchas and best practices

  • Keep names stable: Treat named action names as API. UI tools may reference them; avoid breaking renames.

  • Do not drop toolCallId: If you manually construct intermediate actions, ensure the options object is preserved so llmToolCallResponse can read toolCallId.

  • Use small, composable chains: Prefer short, reusable named actions and compose with additional schema actions when needed.

Best Practices

Sizing and Styling

Generally, it should be the implementation of the component that is responsible for the component's size. There are some exceptions. This means that the width of your component is generally full width and the parent or 'implementor' of the component will control the width.

Examples:

Component Type
Size controller
Comments

Page Header Component

implementation

this allows the header

Button Component

Defined in component

Buttons generally don't take on the shape of the parent elements. When you need to override a button's width, you can pass in a class.

Last updated