jfb-settings-tab

Registers a custom settings tab in the JetFormBuilder admin Settings page using the official JFB API — PHP-side Base_Handler subclass for persistence, JS-side wp.hooks filter for the Vue tab component, native cx-vui field components for the UI. Use when a plugin needs its own configuration tab inside JFB Settings (API credentials, defaults, debug flags, third-party integrations) and the developer must NOT roll their own admin page. Triggers on mentions of "JetFormBuilder settings tab", "JFB settings page", "Base_Handler", "register-tabs-handlers", "jet.fb.register.settings-page.tabs", "cx-vui-input", or when a JFB-companion plugin is being scaffolded.

Best use case

jfb-settings-tab is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Registers a custom settings tab in the JetFormBuilder admin Settings page using the official JFB API — PHP-side Base_Handler subclass for persistence, JS-side wp.hooks filter for the Vue tab component, native cx-vui field components for the UI. Use when a plugin needs its own configuration tab inside JFB Settings (API credentials, defaults, debug flags, third-party integrations) and the developer must NOT roll their own admin page. Triggers on mentions of "JetFormBuilder settings tab", "JFB settings page", "Base_Handler", "register-tabs-handlers", "jet.fb.register.settings-page.tabs", "cx-vui-input", or when a JFB-companion plugin is being scaffolded.

Teams using jfb-settings-tab should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/jfb-settings-tab/SKILL.md --create-dirs "https://raw.githubusercontent.com/nvdigitalsolutions/mcp-ai-wpoos/main/addons/pro/includes/bundled-skills/jfb-settings-tab/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/jfb-settings-tab/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How jfb-settings-tab Compares

Feature / Agentjfb-settings-tabStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Registers a custom settings tab in the JetFormBuilder admin Settings page using the official JFB API — PHP-side Base_Handler subclass for persistence, JS-side wp.hooks filter for the Vue tab component, native cx-vui field components for the UI. Use when a plugin needs its own configuration tab inside JFB Settings (API credentials, defaults, debug flags, third-party integrations) and the developer must NOT roll their own admin page. Triggers on mentions of "JetFormBuilder settings tab", "JFB settings page", "Base_Handler", "register-tabs-handlers", "jet.fb.register.settings-page.tabs", "cx-vui-input", or when a JFB-companion plugin is being scaffolded.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# JetFormBuilder: register a custom settings tab

JetFormBuilder ships with its own Vue-based admin Settings page. Companion plugins should not create separate `add_options_page()` screens — they should plug into the JFB Settings page as a tab. This skill describes the official API for doing so.

## API stability note

The `Base_Handler` PHP API and the `jet.fb.register.settings-page.tabs` JS filter described here are stable across the entire JFB 3.x line and have not had breaking changes in the version range observed. The `cx-vui-*` Vue components are also stable. The `plugin-version-tested` field in the frontmatter records only the version where this skill was last verified end-to-end — treat it as "this skill was confirmed accurate on at least this version", not as "this is the only version where it works". Almost certainly this skill applies to JFB 3.0+ unchanged. If you encounter a JFB version where any step here is wrong, please file an issue and bump the field — don't silently work around it.

The flow has **two halves** that must both be implemented:

1. **PHP side** — a `Base_Handler` subclass for persistence + the `jet-form-builder/register-tabs-handlers` filter to register it.
2. **JS side** — a Vue component + the `jet.fb.register.settings-page.tabs` JS filter (via `wp.hooks.addFilter`) to render it.

Skip either half and the tab silently won't appear or won't save.

## When to use this skill

- A companion plugin needs a configuration tab inside JFB Settings.
- The user mentions API keys, OAuth, third-party integrations that need a settings UI for JFB.
- The diff/files contain `Base_Handler`, `register-tabs-handlers`, `jet.fb.register.settings-page.tabs`, `cx-vui-*`, or `Pages_Manager`.

## Architecture in one paragraph

`Tab_Handler_Manager` (singleton) collects tab handlers via the `jet-form-builder/register-tabs-handlers` filter. Each handler is a `Base_Handler` subclass with a `slug()`, `on_load()` (returns saved options to Vue), and `on_get_request()` (saves POST data). On the JS side, the JFB admin bundle reads a Vue-side filter `jet.fb.register.settings-page.tabs` to collect tab components. Each component receives saved options as `props.incoming` and exposes a `getRequestOnSave()` method that returns `{ data: {...} }`. JFB persists the data to `wp_options` under the key `jet_form_builder_settings__<slug>` as JSON, via an AJAX action `wp_ajax_jet_fb_save_tab__<slug>` with nonce `jfb-settings` and capability `manage_options`.

## Step 1 — PHP: extend Base_Handler

```php
<?php
namespace MyPlugin\Settings;

use Jet_Form_Builder\Admin\Tabs_Handlers\Base_Handler;

class SettingsTab extends Base_Handler {

    public function slug() {
        return 'my-plugin-settings-tab';
    }

    public function on_load() {
        // Returned to Vue as props.incoming
        return $this->get_options( SettingsRepository::defaults() );
    }

    public function on_get_request() {
        // Sanitize $_POST input (already nonce + cap-checked by Base_Handler)
        $payload = array(
            'api_key'       => sanitize_text_field( wp_unslash( $_POST['api_key'] ?? '' ) ),
            'debug_enabled' => rest_sanitize_boolean( $_POST['debug_enabled'] ?? false ),
        );

        $this->update_options( $payload );
        $this->send_response( $this->get_success_response_data() );
    }
}
```

Required methods: `slug`, `on_load`, `on_get_request`. The `slug` MUST match the JS-side tab identifier and must be URL-safe.

`Base_Handler` already verifies the `jfb-settings` nonce and `manage_options` capability before calling `on_get_request`. **Do not skip your own input sanitization** — the cap check guards who can save, not what they save.

## Step 2 — PHP: register the handler

```php
add_filter(
    'jet-form-builder/register-tabs-handlers',
    function ( array $tabs ): array {
        $tabs[] = new \MyPlugin\Settings\SettingsTab();
        return $tabs;
    }
);
```

Hook this in your plugin's main bootstrap, NOT inside `init` — JFB collects tabs early. A safe place is the constructor of your plugin's main class instantiated on `plugins_loaded` priority 11+.

## Step 3 — PHP: enqueue assets for your tab

```php
add_action( 'jet-fb/admin-pages/before-assets/jfb-settings', function () {
    wp_enqueue_style(
        \Jet_Form_Builder\Admin\Pages\Pages_Manager::STYLE_ADMIN
    );
    wp_enqueue_script(
        \Jet_Form_Builder\Admin\Pages\Pages_Manager::SCRIPT_VUEX_PACKAGE
    );
    wp_enqueue_script(
        \Jet_Form_Builder\Admin\Pages\Pages_Manager::SCRIPT_PACKAGE
    );

    wp_enqueue_script(
        'my-plugin-settings-tab',
        plugins_url( 'assets/js/settings-tab.js', __FILE__ ),
        array(
            \Jet_Form_Builder\Admin\Pages\Pages_Manager::SCRIPT_VUEX_PACKAGE,
            'wp-hooks',
            'wp-i18n',
            // 'wp-api-fetch' — only if your JS actually calls wp.apiFetch
        ),
        '1.0.0',
        true
    );
});
```

Always declare `wp-hooks` and `wp-i18n` as dependencies even if you "see them on `window.wp`" — they're what `addFilter` and `__()` rely on. Add `wp-api-fetch` only when your component actually calls `wp.apiFetch` (fetch wrappers, REST calls). Including the JFB Vuex package handle as a dependency is also recommended so script load order is deterministic.

## Step 4 — JS: register the Vue tab component

```js
import { addFilter } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';

const MyPluginSettingsTab = {
    props: {
        incoming: { type: Object, required: true },
    },
    data() {
        return {
            current: { ...this.incoming },
        };
    },
    methods: {
        // JFB calls this on save; must return { data: ... }
        getRequestOnSave() {
            return { data: this.current };
        },
    },
    render( h ) {
        return h( 'cx-vui-panel', [
            h( 'cx-vui-input', {
                attrs: {
                    label: __( 'API Key', 'my-plugin' ),
                    description: __( 'Get this from your dashboard.', 'my-plugin' ),
                    size: 'fullwidth',
                    'wrapper-css': [ 'equalwidth' ],
                },
                model: {
                    value: this.current.api_key,
                    callback: ( v ) => { this.current.api_key = v; },
                },
            } ),
            h( 'cx-vui-switcher', {
                attrs: { label: __( 'Debug mode', 'my-plugin' ) },
                model: {
                    value: this.current.debug_enabled,
                    callback: ( v ) => { this.current.debug_enabled = v; },
                },
            } ),
        ] );
    },
};

addFilter(
    'jet.fb.register.settings-page.tabs',
    'my-plugin/settings-tab',
    ( tabs ) => {
        tabs.push( {
            title: __( 'My Plugin', 'my-plugin' ),
            component: MyPluginSettingsTab,
        } );
        return tabs;
    }
);
```

The third argument to `addFilter` is your unique namespace — convention is `<plugin-slug>/settings-tab`.

## Native field components — use these 1:1

These are the only Vue components JFB ships that you can use directly without bundling extra UI. They're from Crocoblock's private `cx-vui` library; **not** WordPress core components.

| Component | Purpose | Required attrs | Model binding |
|---|---|---|---|
| `cx-vui-input` | Text / number input | `label`, `description`, `size: 'fullwidth'` | `value` (string \| number) |
| `cx-vui-switcher` | Boolean on/off toggle | `label` | `value` (boolean) |
| `cx-vui-panel` | Collapsible section / container | `label` (optional) | none — just children |
| `cx-vui-button` | Action button | `button-style`, `size` | none — listen for `@click` |

Confirmed in JFB 3.6.0 source and used in two production companion plugins (`media-storage-for-jetformbuilder`, `google-sheet-for-jetformbuilder`).

**Common attrs that work across cx-vui-input / cx-vui-switcher:**

- `label` — string, displayed left of the field.
- `description` — string, help text below.
- `size` — `'fullwidth'` is the only documented value seen in the wild.
- `wrapper-css` — array of class names; `[ 'equalwidth' ]` aligns label and field columns.
- `disabled` — boolean.

`cx-vui-input` `attrs.type`: `'text'` (default), `'number'`, `'password'`. Browser-level types only — no built-in masking.

## Beyond the native set — "hack zone"

JFB does **not** ship native components for: textarea, select / dropdown, repeater, color picker, media picker, code editor, date picker, range slider. If you need these, you have three options, in order of preference:

### Preferred: raw HTML in render function

Acceptable for textarea, simple selects, native HTML5 inputs:

```js
render( h ) {
    return h( 'cx-vui-panel', [
        // Use cx-vui-input where you can
        h( 'cx-vui-input', { /* ... */ } ),

        // Drop down to raw HTML for what cx-vui doesn't cover
        h( 'div', { class: 'cx-vui-component cx-vui-component--equalwidth' }, [
            h( 'label', { class: 'cx-vui-component__label' }, __( 'Notes', 'my-plugin' ) ),
            h( 'textarea', {
                attrs: { rows: 6, class: 'cx-vui-input' },
                domProps: { value: this.current.notes },
                on: { input: ( e ) => { this.current.notes = e.target.value; } },
            } ),
        ] ),
    ] );
},
```

This is what `google-sheet-for-jetformbuilder` does for its credentials JSON textarea. It looks visually consistent if you mirror the cx-vui wrapper class structure (`cx-vui-component`, `cx-vui-component__label`, `cx-vui-input`).

### Acceptable: bundle your own Vue component

For complex UI (custom file picker, OAuth-aware connector card, JSON editor) write a local Vue 2 component and use it in your render function. Keep it inside your plugin's bundle — don't expose it globally.

`media-storage-for-jetformbuilder` does this for its provider connection cards (Dropbox OAuth flow with postMessage).

### Avoid: WordPress `wp-components` (React)

JFB Settings is Vue 2. Mounting React components inside a Vue tree works but creates two virtual DOMs, breaks form-state propagation, and won't participate in JFB's save flow. Don't do it unless there's no alternative — and if you do, keep React contained to a leaf node and bridge state manually.

## Critical rules

- **Slug must match** between PHP `slug()` return value and the option name JFB derives (`jet_form_builder_settings__<slug>`). It also functions as the AJAX action suffix. Use kebab-case, no spaces, no underscores in user-visible parts.
- **`getRequestOnSave()` MUST return `{ data: {...} }`.** Returning the data object directly silently saves nothing.
- **`props.incoming` is read-only initial state.** Mutating it directly does not trigger Vuex reactivity correctly — copy into `data().current` and bind to that.
- **Sanitize in PHP, not JS.** The Vue layer can be bypassed by anyone with `manage_options` who crafts the AJAX request manually.
- **Capability is hardcoded to `manage_options`.** There is no per-tab cap filter. If you need a different role to access the tab, add the check in `on_get_request()` and return `wp_send_json_error()`.
- **Don't use a separate option key.** `Base_Handler::update_options()` already prefixes correctly. Calling `update_option('my_custom_key', ...)` from inside `on_get_request()` defeats the entire system.
- **Asset enqueue must include `Pages_Manager::SCRIPT_PACKAGE`.** Without it, `cx-vui-*` components are not registered and Vue throws "Unknown custom element" warnings on render.

## Save flow — what actually happens

1. User clicks Save in the Vue UI.
2. JFB collects `getRequestOnSave()` from each registered tab component.
3. JFB POSTs to `admin-ajax.php` with action `jet_fb_save_tab__<slug>` + nonce `jfb-settings` + the `data` object flattened as POST fields.
4. `Base_Handler::on_raw_request()` verifies nonce and `manage_options` capability.
5. Your `on_get_request()` runs — you sanitize and call `$this->update_options( $payload )`.
6. `update_options()` JSON-encodes and writes to `wp_options` under `jet_form_builder_settings__<slug>`.
7. `send_response()` returns success/error JSON; the Vue UI displays the toast.

## Common pitfalls (failure modes inferred from the API contract)

- **Tab appears empty / no Save button**: missing `Pages_Manager::SCRIPT_PACKAGE` enqueue. Re-check Step 3.
- **Save returns 403**: nonce expired or wrong capability. Don't override the nonce; let JFB handle it.
- **Save returns success but nothing persists**: `getRequestOnSave()` returned wrong shape. Must be `{ data: {...} }`.
- **Tab loads with empty fields on second visit**: `on_load()` returning `false` or `null` instead of an array. Always return an array, default to `SettingsRepository::defaults()` (your own static defaults class).
- **Filter callback never fires**: registered `jet-form-builder/register-tabs-handlers` too late (after JFB initialized). Move to `plugins_loaded` priority 11.
- **Linking to your tab from outside (`Configure` action link, plugin row meta, dashboard widget, etc.) throws `Not_Found_Page_Exception: Current page is not defined`**: you used `admin.php?page=jfb-settings`. JFB does NOT register its settings under the top-level `admin.php` route — it lives under the `jet-form-builder` CPT submenu. See *Linking to your tab from outside* below for the correct URL.

## Linking to your tab from outside

A common companion-plugin pattern is to add a **Configure** action link on the `Plugins` screen, or a `Settings` link in a dashboard widget, that jumps directly to your settings tab. Two non-obvious things bite people here:

### 1. Use the CPT-submenu URL, not `admin.php`

JFB's settings page is registered as a child of the `jet-form-builder` custom post type, so the URL is:

```
edit.php?post_type=jet-form-builder&page=jfb-settings#<your-tab-slug>
```

NOT `admin.php?page=jfb-settings...`. The `admin.php` form throws `Jet_Form_Builder\Admin\Exceptions\Not_Found_Page_Exception` because the page is not registered there.

### 2. Use the hash fragment for tab selection

The Vue settings page reads `window.location.hash` to pick the active tab on load. There is no `&tab=...` query parameter. The hash value is the tab `slug()` returned by your `Base_Handler` subclass.

### Reference snippet — `Configure` link on the Plugins screen

```php
add_filter(
    'plugin_action_links_' . plugin_basename( __FILE__ ),
    static function ( array $links ): array {
        $url = admin_url(
            'edit.php?post_type=jet-form-builder&page=jfb-settings#my-plugin-settings-tab'
        );
        array_unshift(
            $links,
            sprintf(
                '<a href="%s">%s</a>',
                esc_url( $url ),
                esc_html__( 'Configure', 'my-plugin' )
            )
        );
        return $links;
    }
);
```

`array_unshift` puts `Configure` before `Deactivate`, matching the WordPress convention for settings-bearing plugins (`woocommerce`, `google-sheet-for-jetformbuilder`, etc.).

If you want it in the right-hand row meta column instead (next to *Visit plugin site*), use `plugin_row_meta` with the same URL — the gotcha about the route is identical.

## Cross-references

- Run **`wp-security-audit`** on the PHP handler before release — `on_get_request()` is a write endpoint with attacker-controlled input.
- Run **`wp-i18n-audit`** on both PHP and JS strings to verify text-domain consistency.

## What this skill does NOT cover

- JFB form builder block development (different subsystem).
- JFB action / preset / post-type registration (separate APIs).
- Custom Vuex store integration with JFB's store.
- React / Gutenberg integration inside the Settings page.
- JFB Pro-only APIs that aren't shipped in the free plugin.

## References

- Base handler: `wp-content/plugins/jetformbuilder/includes/admin/tabs-handlers/base-handler.php`
- Tab manager: `wp-content/plugins/jetformbuilder/includes/admin/tabs-handlers/tab-handler-manager.php`
- Pages manager (asset constants): `wp-content/plugins/jetformbuilder/includes/admin/pages/pages-manager.php`
- Reference implementation in JFB itself: `includes/admin/tabs-handlers/options-handler.php`
- Real-world examples: `media-storage-for-jetformbuilder`, `google-sheet-for-jetformbuilder` (companion plugins by the same author).

Related Skills

webapp-testing

5
from nvdigitalsolutions/mcp-ai-wpoos

Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.

web-artifacts-builder

5
from nvdigitalsolutions/mcp-ai-wpoos

Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.

valyu

5
from nvdigitalsolutions/mcp-ai-wpoos

Search the live web and 36+ specialised data sources including SEC filings, PubMed, ChEMBL, clinical trials, FRED economic indicators, and patent databases. Use when current, authoritative, or paywalled data is required.

ui-ux-pro-max

5
from nvdigitalsolutions/mcp-ai-wpoos

AI-powered design intelligence with 67 UI styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 15+ tech stacks. Generates complete design systems for any product type with industry-specific reasoning rules.

theme-factory

5
from nvdigitalsolutions/mcp-ai-wpoos

Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.

slack-gif-creator

5
from nvdigitalsolutions/mcp-ai-wpoos

Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."

skill-creator

5
from nvdigitalsolutions/mcp-ai-wpoos

Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.

shannon

5
from nvdigitalsolutions/mcp-ai-wpoos

Autonomous AI security pen testing. Executes real exploits against web applications to find SQL injection, XSS, SSRF, authentication flaws, and IDOR vulnerabilities. Reports only confirmed, reproducible findings — no false positives.

remotion

5
from nvdigitalsolutions/mcp-ai-wpoos

Create programmatic videos using React and Remotion. Translate natural language descriptions into working Remotion components for product demos, release announcements, explainer videos, and animated content.

planetscale

5
from nvdigitalsolutions/mcp-ai-wpoos

Design schemas and write queries for PlanetScale serverless MySQL using branch-based workflows. Ensures index coverage, avoids foreign key anti-patterns, and treats every schema change as a reviewable, reversible deploy request.

mcp-builder

5
from nvdigitalsolutions/mcp-ai-wpoos

Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).

karpathy-coding-principles

5
from nvdigitalsolutions/mcp-ai-wpoos

Apply Karpathy-inspired coding behavior guidelines — think before coding, prefer simplicity, make surgical changes, and execute toward verifiable goals. Reduces wrong assumptions, overengineering, and unintended side-effects.