Only this pageAll pages
Powered by GitBook
1 of 60

v5.x

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

The Essentials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Features

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Template Directives

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Advanced

Loading...

Alpine.js

Coming soon.

wire:stream

Coming soon

Releases

In this section, you will find the release notes for each version we release under this major version. If you are looking for the release notes of previous major versions use the version switcher at the top left of this documentation book.

Resources

Current

  • Into The Box 2024 - CBWIRE 4 Presentation

Examples

General

Older

The resources below are dated, and CBWIRE has dramatically changed since its release.

wire:cloak

wire:cloak is a directive that hides elements on page load until Livewire is fully initialized. This is useful for preventing the "flash of unstyled content" that can occur when the page loads before Livewire has a chance to initialize.

Basic Usage

To use wire:cloak, add the directive to any element you want to hide during page load:

Lazy Loading

A slow component can slow down the loading of an entire page. CBWIRE's lazy loading feature allows you to delay loading your components until the page is fully rendered.

Let's create a slow component that users will hate.

Changing this to a lazy-loaded component is as simple as using wire( lazy=true ).

This will cause Livewire to skip this component on the initial page load. Once visible in the viewport, Livewire will request a network to load the component fully.

CBWIRE Examples
Demo - DataTable
Demo - Task List
Demo - Form Validation
GitHub Repository
ForgeBox
API Docs
Issue Tracker
Up and Running Screencast
Into The Box 2021 Presentation
Ortus Webinar 2022

Lazy loading also delays the execution of the onMount() lifecycle method.

Placeholder

As your components load, you often want to display some indicator, such as a spinner, to your users. You can do this easily using the placeholder() method.

By default, CBWIRE inserts an empty <div></div> for your component before it loads.

The placeholder and the main component must use the same root element type, such as a div. You will get rendering errors if you don't use the same root element.

Placeholder using ColdBox view

You can render a ColdBox view for a placeholder using the view() method.

// ./wires/SlowComponent.bx
class extends="cbwire.model.Component" {
    function onMount() {
        sleep( 2000 ); // Make this component slow
    }
}
// ./wires/SlowComponent.cfc
component extends="cbwire.model.Component" {
    function onMount() {
        sleep( 2000 ); // Make this component slow
    }
}
<!--- ./wires/slowcomponent.bxm --->
<bx:output>
    <div>
        <h1>Slow Component</h1>
    </div>
</bx:output>
<!--- ./wires/slowcomponent.cfm --->
<cfoutput>
    <div>
        <h1>Slow Component</h1>
    </div>
</cfoutput>
<!--- ./views/someview.bxm|cfm --->
<div>
    #wire( "SlowComponent" )#
</div>
#wire( name="SlowComponent", lazy=true )#
function onMount() {
    sleep( 2000 ); // Make this component slow
}

function placeholder() {
    return "<div>Loading...</div>";
}
function placeholder() {
    return view( "spinner" ); // renders ./views/spinner.bxm|cfm
}
Dynamic Content

wire:cloak is particularly useful in scenarios where you want to prevent users from seeing uninitialized dynamic content such as elements shown or hidden using wire:show.

<!-- wires/togglePanel.bxm -->
<bx:output>
<div>
    <div wire:show="expanded" wire:cloak>
        <!-- Chevron down icon... -->
    </div>

    <div wire:show=
<!-- wires/togglePanel.cfm -->
<cfoutput>
<div>
    <div wire:show="expanded" wire:cloak>
        <!-- Chevron down icon... -->
    </div>

    <div wire:show=

In the above example, without wire:cloak, both icons would be shown before Livewire initializes. However, with wire:cloak, both elements will be hidden until initialization.

wire:current

The wire:current directive allows you to easily detect and style currently active links on a page, making it simple to highlight the current section of your navigation.

Basic Usage

Add wire:current to navigation links to automatically highlight the active link based on the current page URL:

When a user visits /posts, the "Posts" link will have a stronger font treatment than the other links.

What wire:current Does

When you add wire:current to a link element, CBWIRE automatically:

  • Detects active links: Compares the link's href attribute with the current page URL

  • Applies CSS classes: Adds the specified classes when the link matches the current page

  • Works with wire:navigate: Automatically updates active states when using wire:navigate for page transitions

Available Modifiers

Exact Matching

By default, wire:current uses partial matching, meaning it will be applied if the link and current page share the beginning portion of the URL's path. For example, if the link is /posts, and the current page is /posts/1, the wire:current directive will be applied.

Use the .exact modifier to require an exact URL match:

This prevents the "Dashboard" link from being highlighted when the user visits /posts.

Strict Matching

By default, wire:current removes trailing slashes (/) from its comparison. Use the .strict modifier to enforce strict path string comparison:

Troubleshooting

If wire:current is not detecting the current link correctly, ensure the following:

  • You have at least one CBWIRE component on the page, or have included wireScripts() in your layout

  • The link has a valid href attribute

  • The href path matches the URL structure of your application

wire:current works seamlessly with wire:navigate links and automatically updates active states during page transitions.

How It Works

CBWIRE bridges the gap between server-side BoxLang/CFML code and dynamic frontend experiences. Understanding how this magic happens will help you build better applications and debug issues when they arise.

The CBWIRE Architecture

CBWIRE consists of four main parts working together:

  1. Server-side Components - Your BoxLang/CFML classes that handle data and business logic

Introduction

CBWIRE is a ColdBox module that uses Livewire and Alpine.js to help you build modern, reactive BoxLang and CFML applications in record time without building backend APIs.

CBWIRE revolutionizes BoxLang and CFML web development by bringing reactive UI capabilities directly to your server-side code. Build dynamic, interactive interfaces without writing JavaScript, managing APIs, or dealing with complex frontend frameworks.

Your First Component

Let's create a reactive counter component to demonstrate CBWIRE's power. Download and install , then run these commands:

Query String

Synchronize component data properties with URL query parameters to create shareable, bookmarkable URLs that reflect your component's state. When users modify data properties, the URL automatically updates, and when they visit URLs with query parameters, your component initializes with those values.

Basic Usage

Create a search component that updates the URL as users type:

wire:dirty

The wire:dirty directive shows or hides elements based on whether form data has been modified but not yet synced to the server. This provides instant visual feedback to users about unsaved changes, helping prevent data loss and improving the form experience.

Basic Usage

This contact form demonstrates wire:dirty with unsaved change indicators and targeted property tracking:

Testing

Test your components using TestBox.

CBWIRE includes a beautiful test API to test your front-end components quickly.

Test Example

To start, make sure you extend BaseWireTest in your test spec.

What's New With 3.0

05/18/2023

New Features

Single File Components

Combine component logic and template in a single file using

wire:replace

Livewire's DOM diffing is great for updating existing elements on your page, but occasionally you may need to force some elements to render from scratch to reset internal state.

In these cases, you can use the wire:replace directive to instruct Livewire to skip DOM diffing on the children of an element, and instead completely replace the content with the new elements from the server.

This is most useful in the context of working with third-party JavaScript libraries and custom web components, or when element re-use could cause problems when keeping state.

Basic Usage

Below is an example of wrapping a web component with a shadow DOM with wire:replace

<div wire:cloak>
    This content will be hidden until Livewire is fully loaded
</div>
<nav>
    <a href="/dashboard" wire:current="font-bold text-zinc-800">Dashboard</a>
    <a href="/posts" wire:current="font-bold text-zinc-800">Posts</a>
    <a href="/users" wire:current="font-bold text-zinc-800">Users</a>
</nav>
"!expanded"
wire:cloak
>
<!-- Chevron right icon... -->
</div>
</div>
</bx:output>
"!expanded"
wire:cloak
>
<!-- Chevron right icon... -->
</div>
</div>
</cfoutput>

When users visit /articles?search=javascript&category=tech, the component automatically initializes with those values and performs the search.

Query string synchronization happens automatically when data properties change. You don't need to manually update the URL.

Only properties listed in the queryString array will be synchronized with the URL. Other data properties remain internal to the component.

// wires/ArticleSearch.bx
class extends="cbwire.models.Component" {
    data = {
        "search": "",
        "category": "",
        "results": []
    };
    
    queryString = ["search", "category"];
    
    function onMount() {
        performSearch();
    }
    
    function onUpdate() {
        performSearch();
    }
    
    function performSearch() {
        if (data.search.len() > 2) {
            // Simulate search results
            data.results = [
                {"title": "Article 1", "category": "Tech"},
                {"title": "Article 2", "category": "News"}
            ];
        } else {
            data.results = [];
        }
    }
}
@startWire
and
@endWire
markers.

Module-Aware Components

Components can now be organized within ColdBox modules.

Enhancements

Removed cbValidation Dependency

CBWIRE no longer requires cbValidation, making it more lightweight.

Simplified Property Access

Templates now use direct property access instead of the args scope:

Performance Improvements

  • Skip unnecessary ColdBox view caching/lookups

  • Streamlined engine architecture

  • Removed computedPropertiesProxy for better performance

Bug Fixes

  • Fixed empty string/null values not passing to Livewire

  • Support full-path wire resolution (e.g., appMapping.wires.SomeComponent)

  • Ensure data-updating actions trigger DOM updates

  • .see() / .dontSee() in tests now chain correctly

  • Fix computed property overrides in component tests

Breaking Changes

Property Access

Computed Properties

Validation Constraints

Event Emission

<nav>
    <a href="/" wire:current.exact="font-bold">Dashboard</a>
    <a href="/posts" wire:current="font-bold">Posts</a>
</nav>
<nav>
    <a href="/posts/" wire:current.strict="font-bold">Posts</a>
</nav>
<!-- wires/articleSearch.cfm -->
<cfoutput>
<div>
    <h1>Article Search</h1>
    
    <div class="search-form">
        <input wire:model="search" 
               type="search" 
               placeholder="Search articles...">
        
        <select wire:model="category">
            <option value="">All Categories</option>
            <option value="tech">Technology</option>
            <option value="news">News</option>
            <option value="sports">Sports</option>
        </select>
    </div>
    
    <div class="results">
        <h2>Results (#arrayLen(results)#)</h2>
        <cfloop array="#results#" item="article">
            <div class="article">
                <h3>#article.title#</h3>
                <span class="category">#article.category#</span>
            </div>
        </cfloop>
    </div>
</div>
</cfoutput>
// wires/ArticleSearch.cfc
component extends="cbwire.models.Component" {
    data = {
        "search" = "",
        "category" = "",
        "results" = []
    };
    
    queryString = ["search", "category"];
    
    function onMount() {
        performSearch();
    }
    
    function onUpdate() {
        performSearch();
    }
    
    function performSearch() {
        if (len(data.search) > 2) {
            // Simulate search results
            data.results = [
                {"title" = "Article 1", "category" = "Tech"},
                {"title" = "Article 2", "category" = "News"}
            ];
        } else {
            data.results = [];
        }
    }
}
<!-- wires/articleSearch.bxm -->
<bx:output>
<div>
    <h1>Article Search</h1>
    
    <div class="search-form">
        <input wire:model="search" 
               type="search" 
               placeholder="Search articles...">
        
        <select wire:model="category">
            <option value="">All Categories</option>
            <option value="tech">Technology</option>
            <option value="news">News</option>
            <option value="sports">Sports</option>
        </select>
    </div>
    
    <div class="results">
        <h2>Results (#results.len()#)</h2>
        <bx:loop array="#results#" item="article">
            <div class="article">
                <h3>#article.title#</h3>
                <span class="category">#article.category#</span>
            </div>
        </bx:loop>
    </div>
</div>
</bx:output>
<!-- wires/Counter.cfm -->
<cfscript>
    // @startWire
    data = {
        "counter" = 0
    };
    
    function increment() {
        data.counter += 1;
    }
    // @endWire
</cfscript>

<cfoutput>
<div>
    <h2>Counter: #counter#</h2>
    <button wire:click="increment">+</button>
</div>
</cfoutput>
<!-- Load component from specific module -->
#wire("ProductCard@shop", { "productId": 123 })#
#wire("cards.ProductCard@shop", { "productId": 123 })#
<!-- Old (2.x) -->
<cfoutput>#args.name# - #args.computed.getFullName()#</cfoutput>

<!-- New (3.0) -->
<cfoutput>#name# - #getFullName()#</cfoutput>
<!-- Before -->
<cfoutput>#args.name#</cfoutput>
<!-- After -->
<cfoutput>#name#</cfoutput>
<!-- Before -->
<cfoutput>#args.computed.getDisplayName()#</cfoutput>
<!-- After -->
<cfoutput>#getDisplayName()#</cfoutput>
// Before
this.constraints = { "email" = { "required" = true } };
// After
constraints = { "email" = { "required" = true } };
// Before
emitTo("event-name", "ComponentName");
// After
emitTo("ComponentName", "event-name");

Livewire.js - Client-side JavaScript that manages DOM updates and server communication

  • Alpine.js - Lightweight JavaScript framework for client-side interactivity and state management

  • ColdBox Integration - The glue that connects everything within the ColdBox framework

  • Component Lifecycle

    Let's trace through what happens when a CBWIRE component is rendered and interacted with:

    Initial Page Load

    When a user first requests a page containing CBWIRE components:

    1. Component Instantiation: CBWIRE creates an instance of your component class

    2. Data Initialization: The data struct is populated with default values

    3. Mount Lifecycle: If present, the onMount() method executes

    4. Template Rendering: Your component's template is rendered with current data values

    5. HTML Generation: The final HTML is sent to the browser with embedded metadata

    User Interaction Flow

    When a user interacts with your component (clicks a button, submits a form, etc.):

    1. Event Capture: Livewire.js intercepts the DOM event (click, input change, form submit)

    2. Request Preparation: The client serializes current component state and the action to perform

    3. Server Request: An AJAX request is sent to CBWIRE's update endpoint

    4. Component Hydration: CBWIRE recreates your component instance from the serialized state

    5. Action Execution: Your component method runs with the current data context

    6. Data Updates: Component data properties are modified by your action method

    7. Re-rendering: The template is re-rendered with updated data

    8. Response Generation: New HTML and updated state are sent back to the client

    9. DOM Diffing: Livewire.js compares old and new HTML, updating only changed elements

    10. UI Update: The page updates without a full refresh

    Request Anatomy

    Here's what a typical CBWIRE request looks like:

    Outgoing Request

    Server Response

    Data Binding Deep Dive

    CBWIRE provides several ways to bind data between your template and component:

    One-Way Data Binding

    Data flows from component to template automatically:

    Two-Way Data Binding

    Form inputs can automatically sync with component data:

    When users type in these fields, CBWIRE automatically updates your component's data properties in real-time.

    Action Methods

    Actions are public methods in your component that can be called from the template:

    State Management

    CBWIRE's state management is where the magic happens - it seamlessly maintains your component's data across user interactions while keeping everything secure and performant.

    State Serialization: The Journey of Your Data

    Every time a user interacts with your component, CBWIRE performs an intricate dance of data serialization. Your component's data struct becomes a JSON payload that travels between client and server, maintaining perfect synchronization.

    But here's the clever part: only your public data properties make the journey. Private variables, computed properties, and sensitive information stay safely on the server where they belong.

    State Security: Fort Knox for Your Data

    CBWIRE doesn't just send your data into the wild west of the internet unprotected. Every state payload includes a cryptographic checksum - think of it as a tamper-evident seal on your data.

    When a request comes back to the server, CBWIRE immediately verifies this checksum. If someone has tried to modify the data in transit or inject malicious content, the checksum won't match and the request gets rejected faster than you can say "security breach."

    Pro tip: Never store sensitive information like passwords, API keys, or financial data in your component's data struct. Use server-side storage instead:

    State Optimization: Speed Through Smart Design

    The beauty of CBWIRE's state management lies in its efficiency. Unlike traditional frameworks that might send entire page worth of data, CBWIRE only transmits what's actually changed.

    Here's how to keep your components lightning-fast:

    Keep it lean: Your component data should be focused and minimal. Think of it as packing for a trip - only bring what you absolutely need.

    Smart storage strategies: Use the right tool for the job. Component state is for UI-related data that changes frequently. Everything else belongs in more appropriate storage.

    This thoughtful approach to state management is what makes CBWIRE applications feel instantly responsive while maintaining the security and reliability you expect from server-side code.

    Security Model

    CBWIRE includes several security features:

    CSRF Protection

    • Optional CSRF tokens prevent cross-site request forgery

    • Tokens are automatically managed when enabled

    Method Authorization

    Data Validation

    Debugging CBWIRE Applications

    Common Issues and Solutions

    Component not updating?

    • Check browser console for JavaScript errors

    • Verify CBWIRE assets are loaded

    • Ensure action methods are public

    Data not persisting?

    • Remember: each request creates a fresh component instance

    • Use session, cache, or database for persistent data

    • Check if data properties are being overwritten

    Performance problems?

    • Minimize data sent to client

    • Use wire:key for list items

    • Avoid heavy computations in templates

    Development Tools

    • Browser developer tools show CBWIRE requests

    • Server logs contain component errors

    • ColdBox debugger shows component lifecycle

    Advanced Concepts

    Component Communication

    Components can communicate through events:

    Alpine.js Integration

    Combine CBWIRE with Alpine.js for optimal UX:

    Custom Directives

    Extend CBWIRE with custom behavior:

    Understanding these concepts will help you build robust, efficient CBWIRE applications that provide excellent user experiences while maintaining clean, server-side code.

    1. Add Component to Layout

    Insert a counter component into your Main layout using wire("Counter"):

    2. Create the Component

    Define your Counter component:

    3. Create the Template

    Define the counter template:

    4. See the Magic

    Navigate to your application in the browser. Click the + and - buttons to see the counter update instantly without page refreshes or JavaScript code! 🤯

    How Does It Work?

    CBWIRE seamlessly bridges your server-side BoxLang or CFML code with dynamic frontend behavior:

    1. Initial Render: CBWIRE renders your component with its default data values

    2. User Interaction: When a user clicks a button with wire:click, Livewire.js captures the event

    3. Server Request: The interaction triggers an AJAX request to your CBWIRE component's action method

    4. Data Processing: Your server-side action method processes the request and updates component data

    5. Response & Update: The updated HTML is sent back and Livewire.js efficiently updates only the changed parts of the DOM

    This approach gives you the responsiveness of modern JavaScript frameworks while keeping your logic in familiar server-side code.

    Why CBWIRE?

    Building our reactive counter with CBWIRE meant we:

    • ✅ Developed a responsive interface without writing JavaScript

    • ✅ Avoided creating backend APIs - everything stays in your ColdBox application

    • ✅ Eliminated page refreshes while maintaining server-side control

    • ✅ Skipped complex build processes like webpack or JavaScript compilation

    • ✅ Stayed in our BoxLang/CFML environment using familiar syntax and patterns

    Better With Alpine.js

    While CBWIRE handles server communication beautifully, sometimes you need instant client-side updates without server round-trips. This is where Alpine.js shines - a lightweight JavaScript framework designed to work perfectly with Livewire (and therefore CBWIRE).

    Let's enhance our counter to use Alpine.js for instant updates and add persistence:

    Now the counter updates instantly on the client side, but only makes server requests when "Save" is clicked to persist the value. The onMount() method loads the saved counter from the session on page load. Using $wire, Alpine.js communicates seamlessly with your CBWIRE component.

    This combination gives you complete control: instant UI feedback when needed, and server communication when you want to persist data or run complex business logic.

    What's Next?

    Ready to dive deeper? Explore these essential concepts:

    • Getting Started: Complete installation and setup guide

    • Components: Learn how to build and organize CBWIRE components

    • Templates: Master template syntax and data binding

    • Actions: Handle user interactions and component methods

    • : Understand data binding and reactivity

    • : Communicate between components and handle lifecycle events

    Credits

    CBWIRE leverages the incredible JavaScript libraries Livewire and Alpine.js for DOM diffing and client-side functionality. CBWIRE wouldn't exist without the brilliant work of Caleb Porzio, creator of both Livewire and Alpine.js. CBWIRE brings these powerful tools into the ColdBox and BoxLang/CFML ecosystem.

    The CBWIRE module is developed and maintained by Grant Copley, Luis Majano, and the team at Ortus Solutions. Special thanks to Mike Rigsby for his significant contributions to the development and features of CBWIRE 5.

    Project Support

    Please consider becoming one of our Patreon supporters to help us continue developing and improving CBWIRE.

    CommandBox
    <!-- wires/contactForm.bxm -->
    <bx:output>
    <form wire:submit="submit">
        <input type="text" wire:model="name" wire:dirty.class="border-yellow-500" placeholder="Name">
    
    
    <!-- wires/contactForm.cfm -->
    <cfoutput>
    <form wire:submit="submit">
        <input type="text" wire:model="name" wire:dirty.class="border-yellow-500" placeholder="Name">
        <
    

    What wire:dirty Does

    When you add wire:dirty to an element, CBWIRE automatically:

    • Tracks Form Changes: Monitors when form inputs differ from server-side values

    • Shows Elements: Displays elements when data properties have unsaved changes

    • Hides When Synced: Removes elements after data is synchronized with the server

    • Integrates with wire:model: Works seamlessly with two-way data binding

    Available Modifiers

    The wire:dirty directive supports these modifiers:

    • Remove: .remove - Inverts behavior, showing element when data is synced

    • Class: .class - Toggles CSS classes instead of showing/hiding elements

    Combine with wire:target to track specific properties: wire:dirty wire:target="name"

    Use wire:dirty with forms to provide clear feedback about unsaved changes and prevent accidental data loss.

    // wires/ContactForm.bx
    class extends="cbwire.models.Component" {
        data = {
            "name": "",
            "email": "",
            "message": ""
        };
    
        function submit() {
            // Save form data
            sleep(1000);
            // Form is now synced
        }
    }
    // wires/ContactForm.cfc
    component extends="cbwire.models.Component" {
        data = {
            "name" = "",
            "email" = "",
            "message" = ""
        };
    
        function submit() {
            // Save form data
            sleep(1000);
            // Form is now synced
        }
    }
    You can invoke your component by calling wire() and then chain additional state changes and assertions.
    class extends="cbwire.models.BaseWireTest" {
    
        function run(){
            describe( "TaskList.cfc", function(){
    	    it( "calling 'clearTasks' removes the tasks", function() {
                    wire( "TaskList"
    
    component extends="cbwire.models.BaseWireTest" {
    
        function run(){
            describe( "TaskList.cfc", function(){
    	    it( "calling 'clearTasks' removes the tasks", function() {
                    wire( "TaskList"
    

    Test Methods

    data

    Set a data property to the specified value.

    computed

    Set a computed property to the specified closure.

    toggle

    Toggles a data property between true and false.

    call

    Calls an action. An optional array of parameters can be provided.

    emit

    Emits an event and fires any listeners that are defined on the component. An optional array of parameters can be provided, which will be passed on to listeners.

    see

    Verifies a value can be found in the current component rendering. Otherwise, the test fails.

    dontSee

    Verifies a value is not found in the current component rendering. Otherwise, the test fails.

    seeData

    Verifies a data property matches a specified value. Otherwise, the test fails.

    dontSeeData

    Verifies a data property does not match a specified value. Otherwise, test fails.

    so that Livewire completely replaces the element allowing the custom element to handle its own life-cycle:
    <!-- wires/jsonViewer.bxm -->
    <bx:output>
    <form>
        <!-- ... -->
    
        <div wire:replace>
            <!-- This custom element would have its own internal state -->
            <json-viewer>#serializeJSON(someProperty)#</json-viewer>
        </div
    
    <!-- wires/jsonViewer.cfm -->
    <cfoutput>
    <form>
        <!-- ... -->
    
        <div wire:replace>
            <!-- This custom element would have its own internal state -->
            <json-viewer>#serializeJSON(someProperty)#</json-viewer>
        </div
    

    Replace Self

    You can also instruct Livewire to replace the target element as well as all children with wire:replace.self.

    CBWIRE CLI

    Michael Rigsby has created a CommandBox CLI tool for CBWIRE that rapidly scaffolds components, templates, and boilerplate code. The CLI supports both standard components and single-file components with extensive customization options.

    Install the CBWIRE CLI from ForgeBox.

    Installation

    Install via CommandBox like so:

    Basic Usage

    Create a simple component:

    Create a component with data properties and actions:

    Configuration Options

    Component Structure

    Option
    Type
    Description

    Data Properties

    Option
    Type
    Description

    Component Behavior

    Option
    Type
    Description

    JavaScript Integration

    Option
    Type
    Description

    File Management

    Option
    Type
    Description

    Advanced Examples

    Module Component

    Create a component in a specific module:

    Full-Featured Component

    Generate a component with comprehensive features:

    Single-File Component

    Create a single-file component with all features:

    Lazy Loading Component

    Generate a component optimized for lazy loading:

    What's New With 3.2

    12/20/2023

    New Features

    Auto-populate Data Properties

    CBWIRE 3.2 introduces automatic data property population when onMount() isn't defined. This streamlines component development by reducing boilerplate code for simple components.

    Previously, you needed to explicitly define onMount() to ensure data properties were available. Now, CBWIRE automatically handles this initialization for you when passing parameters to components without an onMount() method.

    Module-Specific Component Loading

    Support for wire("MyComponent@module") calls enables loading components from specific modules, improving organization in complex applications.

    Browser Event Dispatching

    CBWIRE 3.2 adds support for dispatching browser events using the new dispatch() method, enabling better integration with JavaScript frameworks and third-party libraries.

    Enhancements

    Improved Component Initialization

    • Streamlined data property setup for components without custom initialization logic

    • Reduced boilerplate code requirements for simple components

    • Better parameter handling with type validation for security

    Module System Integration

    • Enhanced module loading capabilities with @module syntax

    • Support for nested folders within modules

    • Better organization for large applications with multiple modules

    • Seamless integration with existing ColdBox module architecture

    Template Access Improvements

    • Direct access to ColdBox event object in templates via event variable

    • Ability to call event methods directly from templates

    • Enhanced integration between CBWIRE and ColdBox framework

    Breaking Changes

    This release maintains backward compatibility with existing CBWIRE 3.x applications. No breaking changes were introduced in version 3.2.

    Contributors

    Special thanks to Michael Rigsby for contributing the auto-populate data properties feature and module-specific component loading functionality.

    wire:init

    The wire:init directive runs an action automatically when the component is first rendered in the browser. This allows you to load data or perform initialization tasks after the page loads without blocking the initial page render, creating a better user experience for data-heavy components.

    Basic Usage

    This analytics dashboard demonstrates wire:init loading data after the component renders:

    // wires/AnalyticsDashboard.bx
    class extends="cbwire.models.Component" {
        data = {
            "stats": {},
            "loading": true
        };
    
        function loadStats() {
    
    
    // wires/AnalyticsDashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "stats" = {},
            "loading" = true
        };
    
        function loadStats() {
            sleep(1500); // Simulate API call
            data.stats = {
                
    

    What wire:init Does

    When you add wire:init to an element, CBWIRE automatically:

    • Runs After Render: Executes the specified action immediately after the component renders

    • Enables Lazy Loading: Allows heavy data operations without blocking initial page load

    • Triggers Once: Runs the action only on the initial render, not on subsequent updates

    • Improves Performance: Separates fast rendering from slow data loading

    Available Modifiers

    The wire:init directive does not support any modifiers.

    Consider using as a modern alternative that provides more control over when and how components initialize.

    wire:confirm

    The wire:confirm directive adds a confirmation dialog before executing actions, protecting users from accidental clicks on destructive operations. When users click an element with wire:confirm, they'll see a browser confirmation dialog that must be accepted before the action proceeds.

    Basic Usage

    This subscription management component demonstrates wire:confirm for protecting critical actions:

    // wires/SubscriptionManager.bx
    class extends="cbwire.models.Component" {
        data = {
            "subscriptionActive": true,
            "userName": "John Doe"
        };
    
        function cancelSubscription() {
    

    What wire:confirm Does

    When you add wire:confirm to an element, CBWIRE automatically:

    • Shows Confirmation Dialog: Displays a browser confirmation dialog with your custom message

    • Blocks Action Execution: Prevents the action from running if the user cancels

    • Continues on Accept: Executes the action normally if the user confirms

    • Integrates with Actions: Works seamlessly with any wire:click

    Available Modifiers

    The wire:confirm directive supports one modifier:

    • Prompt: .prompt - Requires users to type a specific confirmation text

    Example: wire:confirm.prompt="Type DELETE to confirm|DELETE"

    Use wire:confirm on any destructive or irreversible actions to prevent accidental data loss and improve user experience.

    wire:ignore

    The wire:ignore directive tells CBWIRE to skip updating specific parts of your template during re-renders. This is essential when integrating third-party JavaScript libraries, Alpine.js components, or preserving content that should remain static across component updates.

    Basic Usage

    This dashboard component demonstrates wire:ignore protecting third-party content and preserving static elements:

    // wires/Dashboard.bx
    class extends="cbwire.models.Component" {
        data = {
            "refreshCount": 0,
            "loadTime": now()
        };
    
        function refresh() {
    
    // wires/Dashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "refreshCount" = 0,
            "loadTime" = now()
        };
    
        function refresh() {
            data.refreshCount++;
        }
    }

    What wire:ignore Does

    When you add wire:ignore to an element, CBWIRE automatically:

    • Skips DOM Updates: Prevents the element and its children from being updated during re-renders

    • Preserves Third-Party State: Maintains JavaScript library state and event listeners

    • Protects Static Content: Keeps timestamps, IDs, and other content that should remain unchanged

    • Enables Library Integration: Allows Alpine.js and other frameworks to manage their own DOM sections

    Available Modifiers

    The wire:ignore directive supports one modifier:

    • Self: .self - Ignores updates only to the element's attributes, not its children

    Example: wire:ignore.self protects element attributes while allowing child content to update

    Use wire:ignore when integrating third-party JavaScript libraries or preserving content that should remain static across component updates.

    wire:show

    Livewire's wire:show directive makes it easy to show and hide elements based on the result of an expression.

    The wire:show directive is different than using conditionals in your template in that it toggles an element's visibility using CSS (display: none) rather than removing the element from the DOM entirely. This means the element remains in the page but is hidden, allowing for smoother transitions without requiring a server round-trip.

    Basic Usage

    Here's a practical example of using wire:show to toggle a "Create Post" modal:

    When the "New Post" button is clicked, the modal appears without a server roundtrip. After successfully saving the post, the modal is hidden and the form is reset.

    Using Transitions

    You can combine wire:show with Alpine.js transitions to create smooth show/hide animations. Since wire:show only toggles the CSS display property, Alpine's x-transition directives work perfectly with it:

    The Alpine.js transition classes above will create a fade and scale effect when the modal shows and hides.

    For more information on Alpine.js transitions, visit the .

    What's New With 2.2

    01/09/2022

    New Features

    Hydration Lifecycle Hooks

    CBWIRE 2.2 introduces new hydration lifecycle methods that run when components are restored from their serialized state.

    Templates

    Templates define your component's HTML presentation layer using standard HTML/CFML tags. They're dynamic, reactive views that access your component's data properties and methods to create interactive user experiences.

    CBWIRE automatically looks for template files with the same name as your component. Templates can use BoxLang tags like <bx:if>, <bx:loop>, and <bx:output> or CFML tags like <cfif>, <cfloop>, and <cfoutput> alongside wire directives for reactive behavior.

    Actions

    Actions are methods on your that either change the component's or perform some routine, such as updating your database or anything you can dream up in CFML.

    Here is a basic example of how to use it:

    Actions do not need to return any value. Return values are ignored.

    wire:offline

    The wire:offline directive enables you to create responsive user interfaces that gracefully handle network connectivity changes, providing visual feedback when users go offline or come back online.

    Basic Usage

    This simple component demonstrates wire:offline for showing content when offline, adding CSS classes, and removing CSS classes:

    Computed Properties

    Computed Properties are dynamic properties that are cached per component rendering.

    Differences

    Computed Properties are similar to with some key differences:

    • They are declared as functions within your component with a computed attribute added.

    wire:poll

    The wire:poll directive provides an easy way to automatically refresh content at regular intervals, keeping your application's data current without requiring user interaction or complex real-time technologies.

    Basic Usage

    This simple component demonstrates wire:poll with default intervals, custom timing, background polling, and viewport polling:

    wire:key

    The wire:key directive helps Livewire's DOM diffing engine accurately track elements across re-renders. By providing unique identifiers for dynamic content, especially in loops, wire:key ensures that updates, removals, and reordering work correctly and efficiently.

    Basic Usage

    This simple list demonstrates wire:key for proper DOM tracking in loops:

    wire:submit

    The wire:submit directive provides a seamless way to handle form submissions in your CBWIRE components. Instead of dealing with traditional form processing and page refreshes, wire:submit intercepts form submissions and calls your component methods directly, creating smooth, dynamic form experiences.

    Basic Usage

    This contact form demonstrates wire:submit with form handling and loading states:

    wire:text

    wire:text is a directive that dynamically updates an element's text content based on a component property or expression. Unlike using template output syntax, wire:text updates the content without requiring a network roundtrip to re-render the component.

    If you are familiar with Alpine's x-text directive, the two are essentially the same.

    Basic Usage

    Here's an example of using wire:text

    // wires/UserProfile.bx
    class extends="cbwire.models.Component" {
        data = {
            "user": {
                "name": "John Doe",
                "email": "[email protected]"
            }
        };
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        data = {
            "user" = {
                "name" = "John Doe",
                "email" = "[email protected]"
            }
        };
    }
    // wires/TodoList.bx
    class extends="cbwire.models.Component" {
        data = {
            "todos": [],
            "newTodo": ""
        };
        
        function addTodo() {
            if (data.newTodo.trim().len()) {
                data.todos.append({
                    "id": createUUID(),
                    "text": data.newTodo,
                    "completed": false
                });
                data.newTodo = "";
            }
        }
        
        function toggleTodo(todoId) {
            data.todos.each(function(todo) {
                if (todo.id == arguments.todoId) {
                    todo.completed = !todo.completed;
                }
            });
        }
        
        function removeTodo(todoId) {
            data.todos = data.todos.filter(function(todo) {
                return todo.id != todoId;
            });
        }
    }
    // wires/TodoList.cfc
    component extends="cbwire.models.Component" {
        data = {
            "todos" = [],
            "newTodo" = ""
        };
        
        function addTodo() {
            if (data.newTodo.trim().len()) {
                data.todos.append({
                    "id" = createUUID(),
                    "text" = data.newTodo,
                    "completed" = false
                });
                data.newTodo = "";
            }
        }
        
        function toggleTodo(todoId) {
            data.todos.each(function(todo) {
                if (todo.id == arguments.todoId) {
                    todo.completed = !todo.completed;
                }
            });
        }
        
        function removeTodo(todoId) {
            data.todos = data.todos.filter(function(todo) {
                return todo.id != todoId;
            });
        }
    }
    <!-- Template calling various action methods -->
    <bx:output>
    <div>
        <input type="text" wire:model="newTodo" placeholder="Add a todo...">
        <button wire:click="addTodo">Add</button>
        
        <ul>
            <bx:loop array="#data.todos#" index="todo">
                <li wire:key="todo-#todo.id#">
                    <input type="checkbox" wire:click="toggleTodo('#todo.id#')" #todo.completed ? 'checked' : ''#>
                    <span class="#todo.completed ? 'completed' : ''#">#todo.text#</span>
                    <button wire:click="removeTodo('#todo.id#')">Delete</button>
                </li>
            </bx:loop>
        </ul>
    </div>
    </bx:output>
    <!-- Template calling various action methods -->
    <cfoutput>
    <div>
        <input type="text" wire:model="newTodo" placeholder="Add a todo...">
        <button wire:click="addTodo">Add</button>
        
        <ul>
            <cfloop array="#data.todos#" index="todo">
                <li wire:key="todo-#todo.id#">
                    <input type="checkbox" wire:click="toggleTodo('#todo.id#')" #todo.completed ? 'checked' : ''#>
                    <span class="#todo.completed ? 'completed' : ''#">#todo.text#</span>
                    <button wire:click="removeTodo('#todo.id#')">Delete</button>
                </li>
            </cfloop>
        </ul>
    </div>
    </cfoutput>
    <!-- Example rendered output -->
    <div wire:id="abc123" wire:data='{"counter":0}'>
        <h2>Counter: 0</h2>
        <button wire:click="increment">+</button>
    </div>
    // POST /cbwire/updates
    {
        "fingerprint": {
            "id": "abc123",
            "name": "counter",
            "locale": "en",
            "path": "/",
            "method": "GET"
        },
        "serverMemo": {
            "data": {"counter": 5},
            "checksum": "xyz789"
        },
        "updates": [
            {
                "type": "callMethod",
                "payload": {
                    "method": "increment",
                    "params": []
                }
            }
        ]
    }
    {
        "effects": {
            "html": "<div wire:id=\"abc123\"...>...</div>",
            "dirty": ["counter"]
        },
        "serverMemo": {
            "data": {"counter": 6},
            "checksum": "abc456"
        }
    }
    <!-- Template automatically reflects data changes -->
    <div>
        <h1>Welcome, #data.user.name#</h1>
        <p>Email: #data.user.email#</p>
    </div>
    <!-- Changes to input automatically update component data -->
    <input type="text" wire:model="user.name" />
    <input type="email" wire:model="user.email" />
    // Your component data...
    data = {
        "user": {"name": "Sarah", "role": "admin"},
        "products": [
            {"id": 1, "name": "Widget", "price": 29.99},
            {"id": 2, "name": "Gadget", "price": 49.99}
        ],
        "cartTotal": 0
    };
    
    // Becomes this serialized state
    {
        "data": {
            "user": {"name": "Sarah", "role": "admin"},
            "products": [...],
            "cartTotal": 0
        },
        "checksum": "abc123def456"
    }
    // This travels to the client
    data = {"publicInfo": "visible"};
    
    // These stay on the server
    variables.secretKey = "hidden";
    variables.expensiveCalculation = computeComplexData();
    // Client tries to modify data maliciously
    {
        "data": {"isAdmin": true}, // ← Nice try, hacker!
        "checksum": "originalChecksum" // ← This won't match anymore
    }
    // Result: Request rejected, component stays safe
    // ❌ Bad - exposed to client
    data = {
        "userName": "john",
        "creditCard": "4111-1111-1111-1111" // Yikes!
    };
    
    // ✅ Good - sensitive data stays server-side
    data = {
        "userName": "john",
        "hasPaymentMethod": true
    };
    // Store credit card in encrypted session or database
    session.encryptedPaymentInfo = encryptData(creditCardNumber);
    // ❌ Heavy - will slow down every request
    data = {
        "allUsers": getUserService().getAllUsers(), // 10,000 users!
        "fullProductCatalog": getProductService().getAll(), // Another 5,000 items!
        "searchTerm": ""
    };
    
    // ✅ Light - fast and focused
    data = {
        "searchResults": [], // Empty until needed
        "searchTerm": "",
        "currentPage": 1
    };
    
    function search() {
        // Load data only when needed
        data.searchResults = getUserService().search(data.searchTerm);
    }
    // Component state: UI-focused, changes often
    data = {
        "currentStep": 1,
        "isLoading": false,
        "selectedItems": []
    };
    
    // Session: User-specific, persists across requests
    session.userPreferences = {"theme": "dark", "language": "en"};
    
    // Cache: Expensive computations, shared across users  
    cache.put("popularProducts", getExpensiveProductList(), 60);
    
    // Database: Permanent data, complex relationships
    productService.save(productData);
    // Only public methods can be called from templates
    public function allowedAction() { }
    
    private function protectedAction() { } // Cannot be called via wire:click
    function updateProfile(name, email) {
        // Always validate input data
        if (!isValid("email", arguments.email)) {
            throw("Invalid email address");
        }
        // Process validated data
    }
    // Emit events from one component
    emit("userUpdated", {"userId": 123});
    
    // Listen for events in other components
    listeners = {
        "userUpdated": "handleUserUpdate"
    };
    
    function handleUserUpdate(data) {
        // React to user update
    }
    <!-- Alpine handles client-side interactions -->
    <div x-data="{ count: $wire.counter }">
        <span x-text="count"></span>
        <button @click="count++" x-on:click.debounce.500ms="$wire.save(count)">+</button>
    </div>
    <!-- Custom loading states -->
    <div wire:loading.class="opacity-50">
        Content becomes transparent while updating
    </div>
    
    <!-- Conditional visibility -->
    <div wire:loading wire:target="save">
        Saving...
    </div>
    <!-- layouts/Main.bxm -->
    <bx:output>
    <!doctype html>
    <html>
        <head>
            <title>CBWIRE Demo</title>
        </head>
        <body>
            <!-- Insert our reactive counter -->
            #wire("Counter")#
        </body>
    </html>
    </bx:output>
    <!-- layouts/Main.cfm -->
    <cfoutput>
    <!doctype html>
    <html>
        <head>
            <title>CBWIRE Demo</title>
        </head>
        <body>
            <!-- Insert our reactive counter -->
            #wire("Counter")#
        </body>
    </html>
    </cfoutput>
    // wires/Counter.bx
    class extends="cbwire.models.Component" {
        
        data = {
            "counter": 0
        };
    
        function increment() {
            data.counter++;
        }
        
        function decrement() {
            data.counter--;
        }
    }
    // wires/Counter.cfc
    component extends="cbwire.models.Component" {
        
        data = {
            "counter" = 0
        };
    
        function increment() {
            data.counter++;
        }
        
        function decrement() {
            data.counter--;
        }
    }
    <!-- wires/counter.bxm -->
    <bx:output>
    <div>
        <h1>Count: #counter#</h1>
        <button wire:click="increment">+</button>
        <button wire:click="decrement">-</button>
    </div>
    </bx:output>
    <!-- wires/counter.cfm -->
    <cfoutput>
    <div>
        <h1>Count: #counter#</h1>
        <button wire:click="increment">+</button>
        <button wire:click="decrement">-</button>
    </div>
    </cfoutput>
    // wires/Counter.bx
    class extends="cbwire.models.Component" {   
        
        data = {
            "counter": 0
        };
    
        function onMount() {
            data.counter = session.counter ?: 0;
        }
        
        function save(counter) {
            session.counter = arguments.counter;
        }
    }
    // wires/Counter.cfc
    component extends="cbwire.models.Component" {   
        
        data = {
            "counter" = 0
        };
    
        function onMount() {
            data.counter = session.counter ?: 0;
        }
        
        function save(counter) {
            session.counter = arguments.counter;
        }
    }
    <!-- wires/counter.bxm -->
    <bx:output>
    <div 
        x-data="{
            counter: $wire.counter,
            increment() { this.counter++; },
            decrement() { this.counter--; },
            async save() {
                await $wire.save(this.counter);
            }
        }"
        wire:ignore.self>
        
        <h1>Count: <span x-text="counter"></span></h1>
        <button @click="increment">+</button>
        <button @click="decrement">-</button>
        <button @click="save">Save</button>
    </div>
    </bx:output>
    <!-- wires/counter.cfm -->
    <cfoutput>
    <div 
        x-data="{
            counter: $wire.counter,
            increment() { this.counter++; },
            decrement() { this.counter--; },
            async save() {
                await $wire.save(this.counter);
            }
        }"
        wire:ignore.self>
        
        <h1>Count: <span x-text="counter"></span></h1>
        <button @click="increment">+</button>
        <button @click="decrement">-</button>
        <button @click="save">Save</button>
    </div>
    </cfoutput>
    mkdir cbwire-playground --cd
    install cbwire@4
    install commandbox-boxlang
    coldbox create app
    server start cfengine=boxlang javaVersion=openjdk21_jdk
    wire( "TaskList" )
        .data( "tasks", [ "task1", "task2" ] ) // one property at a time
        .data( { "tasks" : [ "task1", "task2" ] } ); // struct of properties
    wire( "TaskList" )
        .computed( "count", function() { return 3; } ) // one property at a time
        .computed( { "count" : function() { return 3; } } ); // struct of properties
    wire( "TaskList" ).toggle( "showModal" );
    wire( "TaskList" ).call( "clearTasks" );
    wire( "TaskList" ).call( "clearTasks", [ param1, param2 ] );
    wire( "TaskList" ).emit( "addedTask" );
    wire( "TaskList" ).emit( "addedTask", [ param1, param2 ] );
    wire( "TaskList" ).see( "<h1>My Tasks</h1>" );
    wire( "TaskList" ).dontSee( "<h1>Someone Else's Tasks</h1> ");
    wire( "TaskList" ).seeData( "tasksRemoved", true );
    wire( "TaskList" ).dontSeeData( "tasksRemoved", true );
    <div x-data="{open: false}" wire:replace.self>
      <!-- Ensure that the "open" state is reset to false on each render -->
    </div>
    box install cbwire-cli
    <
    div
    wire:dirty
    wire:target
    =
    "name"
    >Name has unsaved changes...</
    div
    >
    <input type="email" wire:model="email" placeholder="Email">
    <textarea wire:model="message" placeholder="Message"></textarea>
    <button type="submit">Submit</button>
    <div wire:dirty>You have unsaved changes. Click submit to save.</div>
    <div wire:dirty.remove>All changes saved!</div>
    </form>
    </bx:output>
    div
    wire:dirty
    wire:target
    =
    "name"
    >Name has unsaved changes...</
    div
    >
    <input type="email" wire:model="email" placeholder="Email">
    <textarea wire:model="message" placeholder="Message"></textarea>
    <button type="submit">Submit</button>
    <div wire:dirty>You have unsaved changes. Click submit to save.</div>
    <div wire:dirty.remove>All changes saved!</div>
    </form>
    </cfoutput>
    )
    // Sets our data properties
    .data( "tasks", [ "task1" ] )
    // Verifies output in our template rendering
    .see( "<li>task 1</li>" )
    // Runs the 'clearTasks' action
    .call( "clearTasks" )
    // Verifies updated template rendering
    .dontSee( "<li>task 1</li>" );
    } );
    } );
    }
    }
    )
    // Sets our data properties
    .data( "tasks", [ "task1" ] )
    // Verifies output in our template rendering
    .see( "<li>task 1</li>" )
    // Runs the 'clearTasks' action
    .call( "clearTasks" )
    // Verifies updated template rendering
    .dontSee( "<li>task 1</li>" );
    } );
    } );
    }
    }
    >
    <!-- ... -->
    </form>
    </bx:output>
    >
    <!-- ... -->
    </form>
    </cfoutput>
    data.refreshCount++;
    }
    }
    <!-- wires/dashboard.bxm -->
    <bx:output>
    <div>
        <h1>Dashboard (Refreshed #refreshCount# times)</h1>
        
        <!-- This content never updates -->
        <div wire:ignore>
            <p>Component first loaded: #dateFormat(loadTime, "yyyy-mm-dd")# #timeFormat(loadTime, "HH:mm:ss")#</p>
            <div id="third-party-chart"></div>
        </div>
        
        <!-- This Alpine.js component preserves its state -->
        <div wire:ignore.self x-data="{ count: 0 }" class="counter">
            <button @click="count++">Alpine Counter: <span x-text="count"></span></button>
            <p>Current time: #timeFormat(now(), "HH:mm:ss")#</p>
        </div>
        
        <button wire:click="refresh">Refresh Component</button>
    </div>
    </bx:output>
    <!-- wires/dashboard.cfm -->
    <cfoutput>
    <div>
        <h1>Dashboard (Refreshed #refreshCount# times)</h1>
        
        <!-- This content never updates -->
        <div wire:ignore>
            <p>Component first loaded: #dateFormat(loadTime, "yyyy-mm-dd")# #timeFormat(loadTime, "HH:mm:ss")#</p>
            <div id="third-party-chart"></div>
        </div>
        
        <!-- This Alpine.js component preserves its state -->
        <div wire:ignore.self x-data="{ count: 0 }" class="counter">
            <button @click="count++">Alpine Counter: <span x-text="count"></span></button>
            <p>Current time: #timeFormat(now(), "HH:mm:ss")#</p>
        </div>
        
        <button wire:click="refresh">Refresh Component</button>
    </div>
    </cfoutput>

    What wire:poll Does

    When you add wire:poll to an element, CBWIRE automatically:

    • Calls your method repeatedly: Executes the specified component method at regular intervals

    • Updates content automatically: Refreshes the display with new data from each polling request

    • Optimizes performance: Intelligently throttles polling when the browser tab is inactive

    • Manages resources: Uses a default 2.5-second interval that balances freshness with server load

    Available Modifiers

    You can customize polling behavior with these modifiers:

    • Timing: .5s, .15s, .5000ms - Set custom polling intervals

    • Background: .keep-alive - Continue polling when browser tab is inactive

    • Viewport: .visible - Poll only when element is visible on screen

    Combine modifiers as needed: wire:poll.10s.keep-alive or wire:poll.15s.visible

    // wires/PollDemo.bx
    class extends="cbwire.models.Component" {
        data = {
            "counter": 0,
            "timestamp": ""
        };
    
        function updateCounter() {
            data.counter++;
            data.timestamp = dateTimeFormat(now(), "HH:mm:ss");
        }
    
        function updateStatus() {
            data.timestamp = dateTimeFormat(now(), "HH:mm:ss");
        }
    }
    // wires/PollDemo.cfc
    component extends="cbwire.models.Component" {
        data = {
            "counter" = 0,
            "timestamp" = ""
        };
    
        function updateCounter() {
            data.counter++;
            data.timestamp = dateTimeFormat(now(), "HH:mm:ss");
        }
    
        function updateStatus() {
            data.timestamp = dateTimeFormat(now(), "HH:mm:ss");
        }
    }

    What wire:key Does

    When you add wire:key to an element, CBWIRE automatically:

    • Tracks Element Identity: Provides unique identifiers for DOM diffing across re-renders

    • Prevents Rendering Issues: Ensures proper updates when elements are added, removed, or reordered

    • Optimizes Performance: Helps Livewire efficiently update only changed elements

    • Maintains State: Preserves element state and event listeners during DOM updates

    Available Modifiers

    The wire:key directive does not support any modifiers.

    Ensure your key names are unique throughout the entire page, not just within your component template. Duplicate keys can cause rendering issues.

    Use wire:key in loops and dynamic content where elements can be added, removed, or reordered. Also use it in conditional blocks when Livewire has trouble detecting DOM changes within if/else statements.

    // wires/ItemList.bx
    class extends="cbwire.models.Component" {
        data = {
            "items": [
                {"id": 1, "name": "Apple"},
                {"id": 2, "name": "Banana"},
                {"id": 3, "name": "Cherry"}
            ]
        };
    
        function removeItem(itemId) {
            data.items = data.items.filter(function(item) {
                return item.id != arguments.itemId;
            });
        }
    }
    // wires/ItemList.cfc
    component extends="cbwire.models.Component" {
        data = {
            "items" = [
                {"id" = 1, "name" = "Apple"},
                {"id" = 2, "name" = "Banana"},
                {"id" = 3, "name" = "Cherry"}
            ]
        };
    
        function removeItem(itemId) {
            data.items = data.items.filter(function(item) {
                return item.id != arguments.itemId;
            });
        }
    }
    sleep(1500); // Simulate API call
    data.stats = {
    "users": 1234,
    "sales": 56789,
    "revenue": 98765
    };
    data.loading = false;
    }
    }
    "users"
    = 1234,
    "sales" = 56789,
    "revenue" = 98765
    };
    data.loading = false;
    }
    }
    Lazy Loading
    action
    data.subscriptionActive = false;
    }
    function deleteAccount() {
    // Permanently delete account logic
    redirect("/goodbye");
    }
    }
    // wires/SubscriptionManager.cfc
    component extends="cbwire.models.Component" {
        data = {
            "subscriptionActive" = true,
            "userName" = "John Doe"
        };
    
        function cancelSubscription() {
            data.subscriptionActive = false;
        }
    
        function deleteAccount() {
            // Permanently delete account logic
            redirect("/goodbye");
        }
    }
    Alpine.js x-transition documentation
    // wires/CreatePost.bx
    class extends="cbwire.models.Component" {
        data = {
            "showModal": false,
            "content": ""
        };
    
        function save() {
            getInstance("Post").create({ "content": data.content });
    
            reset("content");
    
            data.showModal = false;
        }
    }
    // wires/CreatePost.cfc
    component extends="cbwire.models.Component" {
        data = {
            "showModal" = false,
            "content" = ""
        };
    
        function save() {
            getInstance("Post").create({ "content" = data.content });
    
            reset("content");
    
            data.showModal = false;
        }
    }

    Auto-Trim Data Properties

    All string data properties are now automatically trimmed of whitespace, ensuring cleaner data handling.

    JavaScript Component Access

    Components can now be accessed directly from JavaScript using the global cbwire.find() method.

    Turbo SPA Support

    New enableTurbo setting provides single-page application functionality for faster navigation.

    When enabled, page navigation becomes faster with JavaScript-powered transitions instead of full page reloads.

    Enhanced Reset Functionality

    The reset() method can now reset all data properties when called without parameters.

    Enhancements

    Lifecycle Method Standardization

    Updated lifecycle method naming for consistency - mount() is now onMount().

    Improved Performance

    • Faster hydration process with optimized component restoration

    • Better memory management for long-running applications

    • Streamlined event handling and data binding

    Bug Fixes

    Event System Improvements

    • Fixed immediate listener firing: Event listeners no longer fire immediately when emitting from the same component

    • Proper event ordering: Ensured onHydrate() runs before component actions

    • Computed property timing: Fixed computed properties not rendering correctly before actions execute

    Documentation and Structure

    • Fixed DocBox integration: Resolved documentation generation issues caused by file structure changes

    • Better error handling: Improved error messages and debugging information

    Component Lifecycle

    • Hydration timing: Ensured proper order of hydration lifecycle methods

    • State restoration: Fixed issues with component state not being properly restored in certain scenarios

    Breaking Changes

    Lifecycle Method Names

    Update component lifecycle methods from mount() to onMount():

    This change provides better consistency with other lifecycle methods and clearer naming conventions.

    File Structure

    CBWIRE uses automatic file matching for components and templates:

    Template Requirements

    Templates must have a single outer element for proper DOM binding:

    Data Properties

    Access component data properties directly by name:

    Computed Properties

    Define computed properties with the computed annotation and call them as methods:

    Computed properties are cached and only executed when first called or when their dependencies change. See Computed Properties for details.

    Explicit Templates

    Override default template location using the onRender() method:

    Helper Methods

    Access global helper methods from installed ColdBox modules:

    ColdBox Event Object

    Access the ColdBox event object (request context) directly in your templates to use helpful methods like event.buildLink() for generating URLs:

    Be cautious when using event.getCollection() (RC scope) or event.getPrivateCollection() (PRC scope) in templates. CBWIRE fires background requests to /cbwire/update on component re-renders, which may not have access to values set by interceptors or handlers that only run on your primary routes. If you need values from RC or PRC scopes, store them as data properties in your component's onMount() method to ensure they persist across re-renders.

    <!-- wires/greeter.bxm -->
    <bx:output>
    <div>
        <h1>#greeting#</h1>
        <button wire:click="updateGreeting">Update</button>
        
        <bx:if timeOfDay() EQ "morning">
            <p>Good morning!</p>
        <bx:else>
            <p>Good day!</p>
        </bx:if>
    </div>
    </bx:output>
    <!-- wires/greeter.cfm -->
    <cfoutput>
    <div>
        <h1>#greeting#</h1>
        <button wire:click="updateGreeting">Update</button>
        
        <cfif timeOfDay() EQ "morning">
            <p>Good morning!</p>
        <cfelse>
            <p>Good day!</p>
        </cfif>
    </div>
    </cfoutput>
    Executing Actions

    Livewire listens for browser events and invokes actions on your component using directives. These directives are used in your HTML templates and follow the format: wire:[browser event]=[action].

    Some examples of events you can listen for include:

    Event
    Directive

    click

    wire:click

    keydown

    wire:keydown

    submit

    wire:submit

    You can listen for any browser events on elements using the wire:[event] directive, where [event] is the event's name. For example, to listen for a "foo" event on a button element, you would use the following code:

    On some elements, such as forms or links, you need to add a .prevent modifier to prevent the browser's default behavior. Otherwise, the browser will cause the page to reload and you will get unintended results.

    Passing Parameters

    You can pass parameters to actions such as actionName( arg1, arg2, arg3 ).

    The parameter is then passed through to your actions via function arguments.

    Magic Actions

    There are a few magic actions already created on your components.

    Action
    Description

    $refresh

    Will re-render the component without firing any action

    $set( 'dataproperty', value )

    Shortcut to update the value of a property

    $toggle( 'dataproperty' )

    Shortcut to toggle boolean properties off and on

    Consider the example below.

    component
    data properties
    // wires/Time.bx
    class extends="cbwire.models.Component" {
        data = {
            "currentTime" : now()
        };
        function updateTime() {
            data.currentTime = now();
        }
    }
    // wires/Time.cfc
    component extends="cbwire.models.Component" {
        data = {
            "currentTime" : now()
        };
        function updateTime() {
            data.currentTime = now();
        }
    }
    <!--- wires/time.bxm --->
    <bx:output>
        <div>
            <p>Current time: #currentTime#</p>
            <button wire:click="updateTime">Update</button>
        </div>
    </bx:output>
    <!--- wires/time.cfm --->
    <cfoutput>
        <div>
            <p>Current time: #currentTime#</p>
            <button wire:click="updateTime">Update</button>
        </div>
    </cfoutput>
    <!-- wires/pollDemo.bxm -->
    <bx:output>
    <div>
        <!-- Basic polling every 2.5 seconds (default) -->
        <div wire:poll="updateCounter">
            <h3>Auto Counter: #counter#</h3>
            <p>Last updated: #timestamp#</p>
        </div>
        
        <!-- Custom polling interval -->
        <div wire:poll.5s="updateStatus">
            <h3>5-Second Updates</h3>
            <p>Current time: #timestamp#</p>
        </div>
        
        <!-- Keep polling when tab is inactive -->
        <div wire:poll.10s.keep-alive="updateStatus">
            <h3>Background Polling</h3>
            <p>Polls even when tab inactive</p>
        </div>
        
        <!-- Poll only when visible -->
        <div wire:poll.15s.visible="updateStatus">
            <h3>Visible Polling</h3>
            <p>Only polls when on screen</p>
        </div>
    </div>
    </bx:output>
    <!-- wires/pollDemo.cfm -->
    <cfoutput>
    <div>
        <!-- Basic polling every 2.5 seconds (default) -->
        <div wire:poll="updateCounter">
            <h3>Auto Counter: #counter#</h3>
            <p>Last updated: #timestamp#</p>
        </div>
        
        <!-- Custom polling interval -->
        <div wire:poll.5s="updateStatus">
            <h3>5-Second Updates</h3>
            <p>Current time: #timestamp#</p>
        </div>
        
        <!-- Keep polling when tab is inactive -->
        <div wire:poll.10s.keep-alive="updateStatus">
            <h3>Background Polling</h3>
            <p>Polls even when tab inactive</p>
        </div>
        
        <!-- Poll only when visible -->
        <div wire:poll.15s.visible="updateStatus">
            <h3>Visible Polling</h3>
            <p>Only polls when on screen</p>
        </div>
    </div>
    </cfoutput>
    <!-- wires/itemList.bxm -->
    <bx:output>
    <div>
        <bx:loop array="#items#" index="item">
            <div wire:key="item-#item.id#">
                #item.name#
                <button wire:click="removeItem(#item.id#)">Remove</button>
            </div>
        </bx:loop>
    </div>
    </bx:output>
    <!-- wires/itemList.cfm -->
    <cfoutput>
    <div>
        <cfloop array="#items#" index="item">
            <div wire:key="item-#item.id#">
                #item.name#
                <button wire:click="removeItem(#item.id#)">Remove</button>
            </div>
        </cfloop>
    </div>
    </cfoutput>
    // wires/SimpleProfile.cfc
    component extends="cbwire.models.Component" {
        // No need to define onMount() - data properties are auto-populated
        data = {
            "name" = "",
            "email" = "",
            "bio" = ""
        };
        
        function updateProfile() {
            // Save profile logic
            saveUserProfile(data);
        }
    }
    <!-- Load component from specific module -->
    #wire("UserDashboard@admin")#
    
    <!-- Load with parameters from module -->
    #wire("ReportGenerator@reporting", { "type": "monthly" })#
    
    <!-- Load from nested folders within modules -->
    #wire("myComponents.NestedComponent@testingmodule")#
    
    <!-- Standard component loading still works -->
    #wire("StandardComponent")#
    // modules/admin/wires/UserDashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "users" = [],
            "totalUsers" = 0
        };
        
        function onMount() {
            data.users = getUserList();
            data.totalUsers = arrayLen(data.users);
        }
    }
    // wires/NotificationCenter.cfc
    component extends="cbwire.models.Component" {
        data = {
            "notifications" = []
        };
        
        function addNotification(message) {
            arrayAppend(data.notifications, {
                "id" = createUUID(),
                "message" = message,
                "timestamp" = now()
            });
            
            // Dispatch browser event
            dispatch("notification-added", {
                "count" = arrayLen(data.notifications),
                "latest" = message
            });
        }
        
        function clearNotifications() {
            data.notifications = [];
            dispatch("notifications-cleared");
        }
    }
    // Listen for events in your JavaScript
    document.addEventListener('notification-added', function(event) {
        console.log('New notification:', event.detail.latest);
        updateNotificationBadge(event.detail.count);
    });
    
    document.addEventListener('notifications-cleared', function(event) {
        resetNotificationBadge();
    });
    <!-- wires/analyticsDashboard.bxm -->
    <bx:output>
    <div wire:init="loadStats">
        <h1>Analytics Dashboard</h1>
        
        <bx:if loading>
            <div>Loading analytics data...</div>
        <bx:else>
            <div class="stats">
                <div>Users: #stats.users#</div>
                <div>Sales: #stats.sales#</div>
                <div>Revenue: $#stats.revenue#</div>
            </div>
        </bx:if>
    </div>
    </bx:output>
    <!-- wires/analyticsDashboard.cfm -->
    <cfoutput>
    <div wire:init="loadStats">
        <h1>Analytics Dashboard</h1>
        
        <cfif loading>
            <div>Loading analytics data...</div>
        <cfelse>
            <div class="stats">
                <div>Users: #stats.users#</div>
                <div>Sales: #stats.sales#</div>
                <div>Revenue: $#stats.revenue#</div>
            </div>
        </cfif>
    </div>
    </cfoutput>
    <!-- wires/subscriptionManager.bxm -->
    <bx:output>
    <div>
        <h1>Account Settings</h1>
        
        <button wire:click="cancelSubscription" 
                wire:confirm="Are you sure you want to cancel your subscription?">
            Cancel Subscription
        </button>
        
        <button wire:click="deleteAccount" 
                wire:confirm.prompt="Please confirm deletion by typing 'DELETE' below|DELETE">
            Delete Account
        </button>
    </div>
    </bx:output>
    <!-- wires/subscriptionManager.cfm -->
    <cfoutput>
    <div>
        <h1>Account Settings</h1>
        
        <button wire:click="cancelSubscription" 
                wire:confirm="Are you sure you want to cancel your subscription?">
            Cancel Subscription
        </button>
        
        <button wire:click="deleteAccount" 
                wire:confirm.prompt="Please confirm deletion by typing 'DELETE' below|DELETE">
            Delete Account
        </button>
    </div>
    </cfoutput>
    <!-- wires/createPost.bxm -->
    <bx:output>
    <div>
        <button x-on:click="$wire.showModal = true">New Post</button>
    
        <div wire:show="showModal">
            <form wire:submit="save">
                <textarea wire:model="content"></textarea>
    
                <button type="submit">Save Post</button>
            </form>
        </div>
    </div>
    </bx:output>
    <!-- wires/createPost.cfm -->
    <cfoutput>
    <div>
        <button x-on:click="$wire.showModal = true">New Post</button>
    
        <div wire:show="showModal">
            <form wire:submit="save">
                <textarea wire:model="content"></textarea>
    
                <button type="submit">Save Post</button>
            </form>
        </div>
    </div>
    </cfoutput>
    <!-- wires/createPost.bxm -->
    <bx:output>
    <div>
        <button x-on:click="$wire.showModal = true">New Post</button>
    
        <div wire:show="showModal" x-transition.duration.500ms>
            <form wire:submit="save">
                <textarea wire:model="content"></textarea>
                <button type="submit">Save Post</button>
            </form>
        </div>
    </div>
    </bx:output>
    <!-- wires/createPost.cfm -->
    <cfoutput>
    <div>
        <button x-on:click="$wire.showModal = true">New Post</button>
    
        <div wire:show="showModal" x-transition.duration.500ms>
            <form wire:submit="save">
                <textarea wire:model="content"></textarea>
                <button type="submit">Save Post</button>
            </form>
        </div>
    </div>
    </cfoutput>
    // wires/UserForm.cfc
    component extends="cbwire.models.Component" {
        data = {
            "user" = {},
            "originalEmail" = ""
        };
        
        function onMount(params = {}) {
            data.user = getUser(params.userId);
            data.originalEmail = data.user.email;
        }
        
        function onHydrate() {
            // Called every time component is hydrated from state
            logActivity("Form hydrated for user: " & data.user.name);
        }
        
        function onHydrateEmail(value, oldValue) {
            // Called when 'email' property is hydrated
            if (value != data.originalEmail) {
                data.emailChanged = true;
            }
        }
    }
    // wires/ContactForm.cfc
    component extends="cbwire.models.Component" {
        data = {
            "name" = "",
            "email" = "",
            "message" = ""
        };
        
        function submitForm() {
            // All properties are automatically trimmed
            // "  John Doe  " becomes "John Doe"
            // "  [email protected]  " becomes "[email protected]"
            
            if (len(data.name) && len(data.email)) {
                sendContactMessage(data);
            }
        }
    }
    <!-- wires/dashboard.cfm -->
    <cfoutput>
    <div id="dashboard-#args._id#">
        <h2>Dashboard</h2>
        <p>Counter: #counter#</p>
        <button wire:click="increment">Increment</button>
        <button onclick="resetFromJS('#args._id#')">Reset via JS</button>
    </div>
    </cfoutput>
    
    <script>
    function resetFromJS(componentId) {
        // Access component directly from JavaScript
        var component = cbwire.find(componentId);
        component.call('reset');
    }
    
    // You can also access component data
    var dashboard = cbwire.find('#args._id#');
    console.log('Current counter:', dashboard.data.counter);
    </script>
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "enableTurbo" = true  // Enable SPA-like navigation
        }
    };
    // wires/MultiStepForm.cfc
    component extends="cbwire.models.Component" {
        data = {
            "step" = 1,
            "firstName" = "",
            "lastName" = "",
            "email" = "",
            "phone" = ""
        };
        
        function resetForm() {
            // Reset all data properties to their initial values
            reset();
        }
        
        function resetContactInfo() {
            // Reset only specific properties
            reset(["email", "phone"]);
        }
        
        function nextStep() {
            data.step += 1;
        }
        
        function previousStep() {
            data.step -= 1;
        }
    }
    // Old syntax (2.1 and earlier)
    component extends="cbwire.models.Component" {
        function mount(params = {}) {
            // Component initialization
        }
    }
    
    // New syntax (2.2+)
    component extends="cbwire.models.Component" {
        function onMount(params = {}) {
            // Component initialization
        }
    }
    // Before (2.1)
    function mount(params = {}) {
        // initialization code
    }
    
    // After (2.2)
    function onMount(params = {}) {
        // initialization code
    }
    // wires/UserCard.bx
    class extends="cbwire.models.Component" {
        data = {
            "name": "John Doe",
            "email": "[email protected]",
            "isActive": true
        };
    }
    // wires/UserCard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "name" = "John Doe",
            "email" = "[email protected]",
            "isActive" = true
        };
    }
    <!-- wires/userCard.bxm -->
    <bx:output>
    <div class="user-card">
        <h2>#name#</h2>
        <p>Email: #email#</p>
        <bx:if isActive>
            <span class="active">Active User</span>
        </bx:if>
    </div>
    </bx:output>
    <!-- wires/userCard.cfm -->
    <cfoutput>
    <div class="user-card">
        <h2>#name#</h2>
        <p>Email: #email#</p>
        <cfif isActive>
            <span class="active">Active User</span>
        </cfif>
    </div>
    </cfoutput>
    // wires/Dashboard.bx
    class extends="cbwire.models.Component" {
        data = {
            "tasks": [
                {"id": 1, "completed": true},
                {"id": 2, "completed": false}
            ]
        };
    
        function completedCount() computed {
            return data.tasks.filter(function(task) {
                return task.completed;
            }).len();
        }
    }
    // wires/Dashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "tasks" = [
                {"id" = 1, "completed" = true},
                {"id" = 2, "completed" = false}
            ]
        };
    
        function completedCount() computed {
            return arrayFilter(data.tasks, function(task) {
                return task.completed;
            }).len();
        }
    }
    // wires/CustomComponent.bx
    class extends="cbwire.models.Component" {
        function onRender() {
            return template("shared.CustomLayout");
        }
        
        // With parameters
        function onRender() {
            return template("shared.UserCard", {
                "theme": "dark",
                "showAvatar": true
            });
        }
        
        // Inline template
        function onRender() {
            return "<div><h1>Simple Component</h1></div>";
        }
    }
    // wires/CustomComponent.cfc
    component extends="cbwire.models.Component" {
        function onRender() {
            return template("shared.CustomLayout");
        }
        
        // With parameters
        function onRender() {
            return template("shared.UserCard", {
                "theme" = "dark",
                "showAvatar" = true
            });
        }
        
        // Inline template
        function onRender() {
            return "<div><h1>Simple Component</h1></div>";
        }
    }
    <!-- wires/navigation.bxm -->
    <bx:output>
    <nav>
        <ul>
            <li><a href="#event.buildLink('dashboard')#">Dashboard</a></li>
            <li><a href="#event.buildLink('profile')#">Profile</a></li>
            <li><a href="#event.buildLink('settings')#">Settings</a></li>
        </ul>
    </nav>
    </bx:output>
    <!-- wires/navigation.cfm -->
    <cfoutput>
    <nav>
        <ul>
            <li><a href="#event.buildLink('dashboard')#">Dashboard</a></li>
            <li><a href="#event.buildLink('profile')#">Profile</a></li>
            <li><a href="#event.buildLink('settings')#">Settings</a></li>
        </ul>
    </nav>
    </cfoutput>
    ./wires/Counter.bx     <!-- BoxLang class -->
    ./wires/Counter.cfc    <!-- CFML component -->
    ./wires/counter.bxm    <!-- BoxLang template -->
    ./wires/counter.cfm    <!-- CFML template -->
    <!-- ✅ Good: Single outer element -->
    <div>
        <h1>My Component</h1>
        <p>Content here</p>
    </div>
    
    <!-- ❌ Bad: Multiple outer elements -->
    <div>Component header</div>
    <div>Component body</div>
    <!-- Template usage -->
    <div>
        <h1>Task Progress</h1>
        <p>Completed: #completedCount()# of #tasks.len()#</p>
    </div>
    <!-- Using cbi18n module -->
    <div>
        <h1>#$r("welcome.title")#</h1>
        <p>#$r("welcome.message")#</p>
    </div>
    
    <!-- Using cbstorages module -->
    <div>
        <p>Session ID: #getSessionStorage().getId()#</p>
    </div>
    <button wire:foo="someAction">
    <button wire:click="doSomething">Do Something</button>
    
    <input wire:keydown.enter="doSomething">
    
    <form wire:submit="save">
        <button type="submit">Save</button>
    </form>
    <a href="##" wire:click.prevent="doSomething">Do Something</a>
    <div>
        <button wire:click="addTask('Some Task')">Add Task</button>
    </div>
    function addTask( taskName ){
        queryExecute( "
            insert into tasks (
                name
            ) values (
                :name
            )
        ", { name = arguments.taskName } ); // Adds 'Some Task'
    }
    data = {
        "message": ""
    };
    
    function setMessageToHello() {
        data.message = "Hello";
    }
    <div>
        #message#
        <button wire:click="$set( 'message', 'Hello' )">Say Hi</button>
    </div>

    includePlaceholder

    Boolean

    Inserts placeholder action for lazy loading components

    name

    String

    Component name without extensions. Use @module to place in a module's wires directory

    singleFileWire

    Boolean

    Creates a single-file component combining template and logic

    outerElement

    String

    Template outer element type. Defaults to "div"

    description

    String

    Component hint description

    dataProps

    String

    Comma-delimited list of data property keys to generate

    lockedDataProps

    String

    Comma-delimited list of data properties to lock from client modifications

    actions

    String

    Comma-delimited list of action methods to generate

    lifeCycleEvents

    String

    Lifecycle event methods to generate (onRender, onHydrate, onMount, onUpdate)

    onHydrateProps

    String

    Properties to create onHydrate property methods for

    onUpdateProps

    String

    Properties to create onUpdate property methods for

    jsWireRef

    Boolean

    Includes livewire:init & component.init hooks with window._{name} = $wire reference using the name provided in name argument

    wiresDirectory

    String

    Custom directory for wires. Defaults to standard wires directory

    appMapping

    String

    Application root location in web root (e.g., MyApp/)

    open

    Boolean

    Opens generated component and template files

    force

    Boolean

    Overwrites existing files

    <!-- wires/offlineDemo.bxm -->
    <bx:output>
    <div class="app">
        <!-- Connection status indicator -->
        <div wire:offline.class="offline" wire:offline.class.remove="online" 
             class="status-indicator online"
    

    What wire:offline Does

    When you add wire:offline to an element, CBWIRE automatically:

    • Detects connectivity changes: Monitors browser online/offline status in real-time

    • Shows/hides elements: Makes elements visible only when the user goes offline

    • Toggles CSS classes: Adds or removes specified classes based on connection state

    • Provides immediate feedback: Updates the UI instantly when connectivity changes

    Available Modifiers

    You can customize offline behavior with these modifiers:

    • Class addition: .class="offline-style" - Add CSS classes when offline

    • Class removal: .class.remove="online-style" - Remove CSS classes when offline

    Combine both modifiers: wire:offline.class="offline".class.remove="online"

    // wires/OfflineDemo.bx
    class extends="cbwire.models.Component" {
        data = {};
    }
    // wires/OfflineDemo.cfc
    component extends="cbwire.models.Component" {
        data = {};
    }

    They are cached.

  • They can return any CFML data type, not just values that can be parsed by JavaScript like Data Properties.

  • Computed properties are meant to return values and not change state such as modifying Data Properties. If you need to update a data property, use Actions instead.

    Defining Computed Properties

    You can define Computed Properties on your components as functions with the computed attribute added.

    class extends="cbwire.models.Component" {   
    
        // Computed property
        function isEven() computed {
            return data.counter % 2 == 0;
        }
    
    
    component extends="cbwire.models.Component" {   
    
        // Computed property
        function isEven() computed {
            return data.counter % 2 == 0;
        }
    
    

    Templates

    You can access Computed Properties in your component template using propertyName().

    Actions

    You can also access Computed Properties from within your Actions.

    Caching

    Computed Properties cache their results for the lifetime of the request. It will only execute once if you reference your Computed Property three times in your component template or from within a component action.

    You can prevent caching on a computed property by passing a false argument when invoking it.

    Data Properties
    // wires/ContactForm.cfc
    component extends="cbwire.models.Component" {
        data = {
            "name" = "",
            "email" = ""
        };
    
        function submit() {
            // Process form (simulate processing)
            sleep(1000);
    

    What wire:submit Does

    When you add wire:submit to a form, CBWIRE automatically:

    • Prevents Default Submission: Stops the browser from submitting the form traditionally

    • Calls Your Method: Executes the specified component method when the form is submitted

    • Disables Form Elements: Automatically disables submit buttons and marks form inputs as readonly during processing

    • Maintains State: Keeps all your component data intact during the submission process

    Available Modifiers

    You can customize form submission behavior with these modifiers:

    • Prevent: .prevent - Explicitly prevent default form submission (default behavior)

    Example: wire:submit.prevent="saveUser"

    // wires/ContactForm.bx
    class extends="cbwire.models.Component" {
        data = {
            "name": "",
            "email": ""
        };
    
        function submit() {
            // Process form (simulate processing)
            sleep(1000);
            
            // Reset form
            data.name = "";
            data.email = "";
        }
    }
    to optimistically show updates to a Livewire property without waiting for a network roundtrip.
    // wires/ShowPost.bx
    class extends="cbwire.models.Component" {
        data = {
            "postId": 0,
            "likes": 0
        };
    
        function onMount( params ) {
    

    When the button is clicked, $wire.likes++ immediately updates the displayed count through wire:text, while wire:click="like" persists the change to the database in the background.

    This pattern makes wire:text perfect for building optimistic UIs in Livewire.

    Properties
    Events

    Components

    Components are the fundamental building blocks of CBWIRE applications. They encapsulate both the data and behavior needed to create interactive user interface elements that respond to user actions without requiring full page refreshes. Think of components as self-contained, reusable pieces of functionality that manage their own state and handle their own user interactions.

    Components can be as big or small as you like. For example, you may have a Signup form component that covers multiple steps or a simple button component that you reuse throughout your application.

    Each CBWIRE component is a ColdBox component (.bx for BoxLang, .cfc for CFML) that extends the base cbwire.models.Component class. This inheritance provides all the reactive functionality and lifecycle methods needed to create dynamic user interfaces. Components follow a clear structure and naming convention, making them easy to organize and maintain as your application grows.

    Components are made up of data properties, , , and a This separation of concerns keeps your code organized and maintainable while providing the flexibility to create complex interactive experiences.

    When a component is rendered, CBWIRE automatically handles the initial setup, manages the component's lifecycle, and coordinates communication between the server and client. This abstraction allows you to focus on your application's business logic rather than the underlying mechanics of creating reactive interfaces.

    By default, components are placed in the ./wires folder in the project root. You can change this location using the wiresLocation .

    Rendering Components

    You can render a component using the wire() method.

    You can call wire() from within your ColdBox layouts, ColdBox views, and also from your component templates ( See Nesting Components ).

    External Components

    You can render wires from folders and subfolders outside of the default ./wires folder.

    You can also reference components within another ColdBox module by using the @module syntax.

    Passing Parameters

    You can pass data into a component as an additional argument using wire().

    By passing in parameters, you can create reusable UI components that are unique but similar in functionality. For example, you could make a button component that you reuse throughout your app.

    Using Parameters

    Parameters are passed into your component's method. This is an optional method you can add.

    CBWIRE only executes onMount() once when the component is initially rendered. It is not executed for subsequent update requests of the component.

    Auto Populating Data Properties

    Properties you pass into your component as params will be automatically populated only if onMount() is not defined and a matching is found.

    If you define an onMount() method, you must manually set any passed parameters inside the method:

    Breaking Change in CBWIRE 5.0: When onMount() is defined, parameters are no longer automatically set to data properties. You must explicitly assign them inside the onMount() method. This change provides more explicit control over component initialization.

    Passed-in properties must have a data type of string, boolean, numeric, date, array, or struct.

    Nesting Components

    You can nest components as much as you need by simply calling wire() from within a ( See ).

    CBWIREController

    There may be areas of your application where you need to use wire() but don't have access to it. You can use the CBWIREController object to insert wires anywhere you need.

    Lifecycle Methods

    CBWIRE provides lifecycle methods you can hook into to update and render your components.

    Order Of Operations

    The lifecycle methods are executed in the following order when a component is initially loaded:

    1. onSecure()

    2. onMount()

    3. onRender()

    Lifecycle methods are executed in this order for subsequent AJAX requests.

    1. onSecure()

    2. onHydrate[DataProperty]()

    3. onHydrate()

    4. onUpdate[DataProperty]()

    Methods

    onSecure

    Runs before all other lifecycle methods to enforce security rules. Return false to halt processing and render an empty div, or return nothing to continue normally.

    onSecure() fires on every request—both initial rendering and all subsequent AJAX requests. This ensures security checks run continuously throughout the component's lifecycle.

    See the documentation for complete information on securing wire components with onSecure(), cbSecurity integration, and security annotations.

    onMount

    It runs only once when a component is initially wired. This can inject data values into your component via params you pass in when calling wire(), or pulling in values from the RC or PRC scopes.

    onMount() only fires when the component is initially rendered and does not fire on subsequent requests when your component re-renders. This can cause issues referencing things such as the RC or PRC scope. If you pass in values with the RC or PRC scope, you must store them as data properties to ensure they are available to your component in subsequent requests.

    onRender

    It runs on all requests before rendering your component. This gives you more control if needed when rendering. There is also a renderIt() alias, which does the same.

    onHydrate

    Runs on subsequent requests after a component is hydrated but before are rendered, before a is updated or is performed, or before the component is rendered.

    onHydrate[ Property ]

    Runs on subsequent requests after a specific data property is hydrated but before computed properties are rendered, before an action is performed, or before the component is rendered.

    onUpdate

    Runs on subsequent requests after any is updated using wire:model or $set.

    onUpdate() will only fire if the incoming request updates a single data property, such as when using wire:model.

    onUpdate[ Property ]

    Runs on subsequent requests after a is updated using wire:model or $set. It only runs when the targeted data property is updated.

    onUploadError

    Runs automatically when a file upload encounters an error (any HTTP response with a non-2xx status code). This allows you to handle upload failures gracefully in your CBWIRE components.

    Parameter
    Type
    Description

    See the documentation for complete upload error handling information.

    Single-file Components

    Components are typically built with a .bx and a .bxm file (Boxlang ) or a .cfc file and a .cfm file (CFML). You can combine these into a single .bxm or .cfm file, similar to how components in Vue.js are built.

    Let's look at the example from the introduction that uses two separate files.

    // ./wires/Counter.bx
    class extends="cbwire.models.Component" {   
        data = {
            "counter": 0
        };
        
        function onMount() {
            // Load the counter from the session
            data
    
    // ./wires/Counter.cfc
    component extends="cbwire.models.Component" {   
        data = {
            "counter": 0
        };
        
        function onMount() {
            // Load the counter from the session
            data.counter 
    

    Single File Format

    Let's combine these into a single file.

    There are only a few differences with the single-file format.

    • The contents of your .cfc file are instead placed inside a <cfscript> block

    • The // @startWire and // @endWire comment markers have been added so that CBWIRE can parse the file.

    You must add the //@startWire and //@endWire markers when using single-file format.

    You can place your <cfscript> block above or below your template. CBWIRE parses this out before processing.

    Performance

    There is a slight performance hit when using the single file format because CBWIRE has to parse out the sections of your file. However, this hit is minimal because of the internal caching CBWIRE uses.

    Cached files can be found under ./modules/cbwire/models/tmp. This directory gets cleared anytime you reinit your application using ColdBox's fwreinit.

    http://myapp/?fwreinit=true

    wire:click

    The wire:click directive transforms any HTML element into an interactive control that triggers server-side actions. Instead of traditional page refreshes or complex JavaScript event handling, wire:click lets you respond to user clicks by calling component methods directly, creating smooth, dynamic user experiences.

    Basic Usage

    This counter component demonstrates wire:click with basic actions and parameter passing:

    // wires/ClickDemo.bx
    class extends="cbwire.models.Component" {
        data = {
            "count": 0,
            "message": ""
        };
    
        function increment() {
    

    What wire:click Does

    When you add wire:click to an element, CBWIRE automatically:

    • Captures Click Events: Listens for click events on the element without requiring JavaScript

    • Calls Component Methods: Executes the specified action method in your component

    • Passes Parameters: Supports passing arguments to your action methods using parentheses syntax

    • Updates UI: Re-renders the component with any data changes from your action

    Available Modifiers

    The wire:click directive supports one modifier:

    • Prevent: .prevent - Prevents default browser behavior (essential for links)

    Example: wire:click.prevent="sendEmail"

    Use wire:click on any HTML element - buttons, links, divs, images, or any clickable element - making it incredibly versatile for building interactive interfaces.

    Getting Started

    CBWIRE brings reactive UI capabilities to your BoxLang and CFML applications, making it easy to build dynamic interfaces without complex JavaScript frameworks. This guide will help you set up CBWIRE and create your first reactive component.

    System Requirements

    Before installing CBWIRE, ensure your system meets these requirements:

    wire:loading

    The wire:loading directive shows and hides elements during server requests, providing instant visual feedback to users. When actions are processing, you can display loading indicators, disable buttons, or modify the interface to communicate that work is happening behind the scenes.

    Basic Usage

    This form demonstrates wire:loading with various loading states and targeting options:

    Data Binding

    You can bind your to input elements within your using .

    Resources

    cbwire create wire myComponent
    cbwire create wire name="userProfile" dataProps="name,email,isActive" actions="save,cancel" --open
    cbwire create wire name="dashboard@AdminModule" dataProps="stats,alerts" actions="refreshData"
    cbwire create wire name="userManager" \
      dataProps="users,selectedUser,searchTerm" \
      lockedDataProps="selectedUser" \
      actions="loadUsers,selectUser,deleteUser" \
      lifeCycleEvents="onMount,onUpdate" \
      onUpdateProps="searchTerm" \
      description="User management component" \
      --jsWireRef --open
    cbwire create wire name="taskList" \
      dataProps="tasks,newTask,filter" \
      lockedDataProps="tasks" \
      actions="addTask,toggleTask,removeTask" \
      outerElement="section" \
      lifeCycleEvents="onMount,onRender" \
      --singleFileWire --jsWireRef --open
    cbwire create wire name="heavyChart" \
      dataProps="chartData,isLoaded" \
      actions="loadChart" \
      --includePlaceholder --open
    <!-- wires/offlineDemo.cfm -->
    <cfoutput>
    <div class="app">
        <!-- Connection status indicator -->
        <div wire:offline.class="offline" wire:offline.class.remove="online" 
             class="status-indicator online">
            <span class="status-text">Connected</span>
        </div>
    
        <!-- Offline message - only shows when offline -->
        <div wire:offline class="offline-banner">
            <h3>You're currently offline</h3>
            <p>Some features may be unavailable.</p>
        </div>
    
        <!-- Button that gets disabled when offline -->
        <button wire:offline.class="disabled" class="sync-button">
            Sync Data
        </button>
    </div>
    </cfoutput>
    <bx:output>
        <div>
            Is Even: #isEven()#
        </div>
    </bx:output>
    <cfoutput>
        <div>
            Is Even: #isEven()#
        </div>
    </cfoutput>
    // Computed property
    function isEven() computed {
        return data.counter % 2 == 0;
    }
    
    // Action
    function increment() {
        if ( isEven() ) {
            data.counter += 2;
        } else {
            data.counter += 1;
        }
    }
    // Computed properties
    function getUUID() computed {
        return createUUID();
    }
    <div>
        <p>#getUUID()#</p>
        <p>#getUUID()#</p> <!-- Outputs the same ID because of caching -->
    </div>
    <div>
        <p>#getUUID()#</p>
        <p>#getUUID( false )#</p> <!-- Does not have the same ID because caching disabled -->
    </div>
    <!-- wires/contactForm.bxm -->
    <bx:output>
    <form wire:submit="submit">
        <input type="text" wire:model="name" placeholder="Your Name">
        <input type="email" wire:model="email" placeholder="Email">
        
        <button type="submit">
            Send
            <span wire:loading>ing...</span>
        </button>
    </form>
    </bx:output>
    <!-- wires/contactForm.cfm -->
    <cfoutput>
    <form wire:submit="submit">
        <input type="text" wire:model="name" placeholder="Your Name">
        <input type="email" wire:model="email" placeholder="Email">
        
        <button type="submit">
            Send
            <span wire:loading>ing...</span>
        </button>
    </form>
    </cfoutput>
    // wires/ShowPost.cfc
    component extends="cbwire.models.Component" {
        data = {
            "postId" = 0,
            "likes" = 0
        };
    
        function onMount( params ) {
            data.postId = params.postId;
    
            var post = getInstance("Post").findOrFail(data.postId);
            data.likes = post.getLikeCount();
        }
    
        function like() {
            var post = getInstance("Post").findOrFail(data.postId);
            post.like();
    
            data.likes = post.fresh().getLikeCount();
        }
    }
    <!-- wires/showPost.bxm -->
    <bx:output>
    <div>
        <button x-on:click="$wire.likes++" wire:click="like">❤️ Like</button>
    
        Likes: <span wire:text="likes"></span>
    </div>
    </bx:output>
    <!-- wires/showPost.cfm -->
    <cfoutput>
    <div>
        <button x-on:click="$wire.likes++" wire:click="like">❤️ Like</button>
    
        Likes: <span wire:text="likes"></span>
    </div>
    </cfoutput>
    <!--- ./wires/counter.bxm|cfm --->
    <div
        x-data="{
            counter: $wire.counter,
            increment() {
                this.counter++   
            },
            async save() {
                // Call the save method on our component
                await $wire.save( this.counter );
            }
        }"
        wire:ignore.self>
        <div>Count: <span x-text="counter"></span></div>
        <button @click="increment">+</button>
        <button @click="save">Save</button>
    </div>
    >
    <span class="status-text">Connected</span>
    </div>
    <!-- Offline message - only shows when offline -->
    <div wire:offline class="offline-banner">
    <h3>You're currently offline</h3>
    <p>Some features may be unavailable.</p>
    </div>
    <!-- Button that gets disabled when offline -->
    <button wire:offline.class="disabled" class="sync-button">
    Sync Data
    </button>
    </div>
    </bx:output>
    }
    }
    // Reset form
    data.name = "";
    data.email = "";
    }
    }
    data.postId = params.postId;
    var post = getInstance("Post").findOrFail(data.postId);
    data.likes = post.getLikeCount();
    }
    function like() {
    var post = getInstance("Post").findOrFail(data.postId);
    post.like();
    data.likes = post.fresh().getLikeCount();
    }
    }

    Provides Loading States: Integrates with wire:loading for visual feedback during actions

    data.count++;
    }
    function setMessage(text) {
    data.message = arguments.text;
    }
    }
    // wires/ClickDemo.cfc
    component extends="cbwire.models.Component" {
        data = {
            "count" = 0,
            "message" = ""
        };
    
        function increment() {
            data.count++;
        }
    
        function setMessage(text) {
            data.message = arguments.text;
        }
    }
    <!-- wires/clickDemo.bxm -->
    <bx:output>
    <div>
        <h1>Count: #count#</h1>
        <button wire:click="increment">+</button>
        <button wire:click="setMessage('Hello!')">Say Hello</button>
        <a href="#home" wire:click.prevent="setMessage('Link clicked!')">Click Link</a>
        <p>#message#</p>
    </div>
    </bx:output>
    <!-- wires/clickDemo.cfm -->
    <cfoutput>
    <div>
        <h1>Count: #count#</h1>
        <button wire:click="increment">+</button>
        <button wire:click="setMessage('Hello!')">Say Hello</button>
        <a href="##home" wire:click.prevent="setMessage('Link clicked!')">Click Link</a>
        <p>#message#</p>
    </div>
    </cfoutput>
    computed properties
    actions
    template.
    configuration setting
    onMount()
    data property
    template
    Nesting Components
    // ./wires/Counter.bx
    class extends="cbwire.models.Component" {
    
        // Data properties
        data = {
            "counter": 0 // default value
        };
        
        // Action
        function increment() {
            data.counter++;
        }
        
        // Helper method also available to template
        function isEven() {
            return data.counter % 2 == 0;
        }
    }
    // ./wires/Counter.cfc
    component extends="cbwire.models.Component" {
    
        // Data properties
        data = {
            "counter": 0 // default value
        };
        
        // Action
        function increment() {
            data.counter++;
        }
        
        // Helper method also available to template
        function isEven() {
            return data.counter % 2 == 0;
        }
    }
    onUpdate()
  • Fire actions

  • onRender()

  • property

    string

    The name of the data property associated with the file input

    errors

    any

    The error response from the server. Will be null unless the HTTP status is 422, in which case it contains the response body

    multiple

    boolean

    Indicates whether multiple files were being uploaded (true) or a single file (false)

    Security
    computed properties
    data property
    action
    data property
    data property
    File Uploads
    class extends="cbwire.models.Component" {
        data = {
            "someValue: ""
        };
        
        function onMount( event, rc, prc, params ){
            data.someValue = params.someValue;
        }
    }
    component extends="cbwire.models.Component" {
        data = {
            "someValue: ""
        };
        
        function onMount( event, rc, prc, params ){
            data.someValue = params.someValue;
        }
    }
    .counter
    =
    session
    .counter
    ?:
    0
    ;
    }
    function save( counter ) {
    // Save the counter to the session
    session.counter = arguments.counter;
    }
    }
    =
    session
    .counter
    ?:
    0
    ;
    }
    function save( counter ) {
    // Save the counter to the session
    session.counter = arguments.counter;
    }
    }
    See the wire:model page for complete documentation.

    When data properties are updated using wire:model, CBWIRE has several methods you can hook into such as onUpdate and onHyrdate ( See Lifecycle Methods ).

    data properties
    template
    wire:model
    // wires/MyComponent.bx
    class extends="cbwire.models.Component" {
        
        // Data properties
        data = {
            "name": ""
        };
    }
    // wires/MyComponent.cfc
    component extends="cbwire.models.Component" {
        
        // Data properties
        data = {
            "name": ""
        };
    }

    What wire:loading Does

    When you add wire:loading to an element, CBWIRE automatically:

    • Shows Elements: Displays loading indicators during server requests

    • Targets Actions: Can target specific actions or all actions by default

    • Modifies Appearance: Toggles classes, attributes, and display properties

    • Provides Feedback: Gives users immediate visual confirmation that actions are processing

    Available Modifiers

    The wire:loading directive supports these modifiers:

    • Remove: .remove - Hides element during loading instead of showing it

    • Class: .class - Toggles CSS classes during loading

    • Class Remove: .class.remove - Removes specific classes during loading

    • Attr: .attr - Toggles attributes like disabled during loading

    • Display Values: .block, .flex, .grid, .inline, .inline-block, .inline-flex, .table - Sets specific display property

    • Delay: .delay - Delays showing indicator (200ms default)

    • Delay Intervals: .delay.shortest (50ms), .delay.shorter (100ms), .delay.short (150ms), .delay.long (300ms), .delay.longer (500ms), .delay.longest (1000ms)

    Targeting Actions

    Use wire:target to specify which actions trigger loading states:

    Multiple action parameters are not supported: wire:target="remove(1),add(1)" will not work.

    By default, wire:loading triggers for any action. Use wire:target to control exactly when loading states appear.

    // wires/DocumentManager.bx
    class extends="cbwire.models.Component" {
        data = {
            "title": "",
            "content": ""
        };
    
        function save() {
            sleep(2000); // Simulate slow save
        }
    
        function delete() {
            sleep(1500); // Simulate deletion
        }
    
        function reset() {
            data.title = "";
            data.content = "";
        }
    }
    // wires/DocumentManager.cfc
    component extends="cbwire.models.Component" {
        data = {
            "title" = "",
            "content" = ""
        };
    
        function save() {
            sleep(2000); // Simulate slow save
        }
    
        function delete() {
            sleep(1500); // Simulate deletion
        }
    
        function reset() {
            data.title = "";
            data.content = "";
        }
    }
    <!--- ./wires/counter.bxm --->
    <bx:output>
        <div>
            <h1>My Counter</h1>
            Counter: #counter#<br>
            Is Even: #isEven()#
            <button wire:click="increment">Increment</button>
        </div>
    </bx:output>
    <!--- ./wires/counter.cfm --->
    <cfoutput>
        <div>
            <h1>My Counter</h1>
            Counter: #counter#<br>
            Is Even: #isEven()#
            <button wire:click="increment">Increment</button>
        </div>
    </cfoutput>
    <!--- ./layouts/Main.bxm|cfm --->
    <body>
        <div>
            #wire( name="ShowPosts" )#
        </div>
    </body>
    <!--- ./views/posts/index.bxm|cfm --->
    <h1>My Posts</h1>
    #wire( name="ShowPosts" )#
    <div>
        #wire( name="myFolder.MyComponent" )#
        #wire( name="myFolder.subFolder.MyComponent" )#
    </div>
    <div>#wire( name="MyComponent@myModule" )#</div>
    <body>
        <div>
            #wire( name="ShowPost", params={ "post": post } )#
            #wire( "ShowPost", { "post": post } )#
        </div>
    </body>
    <!--- ./views/posts/index.bxm|cfm --->
    <div>
        #wire( "ShowPost", { title: "Post 1" } )#
    </div>
    // ./wires/ShowPost.bx
    class extends="cbwire.models.Component" {
        data = {
            "title": ""
        };
    
        function onMount( params ) {
            data.title = params.title;
        }
    }
    // ./wires/ShowPost.cfc
    component extends="cbwire.models.Component" {
        data = {
            "title": ""
        };
        function onMount( params ) {
            data.title = params.title;
        }
    }
    <div>
        #wire( name="ShowPost", params={ title: "Some title" } )#
    </div>
    // ./wires/ShowPost.bx
    class extends="cbwire.models.Component" {
        data = {
            "title": "",
            "author": ""
        };
    
        function onMount( params ) {
            // Must explicitly set parameters when onMount() is defined
            data.title = params.title;
            data.author = params.author;
        }
    }
    // ./wires/ShowPost.cfc
    component extends="cbwire.models.Component" {
        data = {
            "title" = "",
            "author" = ""
        };
    
        function onMount( params ) {
            // Must explicitly set parameters when onMount() is defined
            data.title = params.title;
            data.author = params.author;
        }
    }
    // property injection
    property name="CBWIREController" inject="CBWIREController@cbwire";
    
    // using getInstance
    CBWIREController = getInstance( "CBWIREController@cbwire" );
    
    // Creating a wire from controller
    CBWIREController.wire( "EditablePage", { contentKey: "someValue" } );
    function onSecure( event, rc, prc, isInitial, params ) {
        // Check if user is authenticated
        if ( !auth.check() ) {
            return false;
        }
    }
    function onMount( event, rc, prc, params ){
        data.someValue = params.someValue;
    }
    function onRender() {
        return "<div>direct html</div>;
        // return template( _getViewPath() ); // CBWIRE's default method
        // return template( "some.custom.path" );
    }
    function onHydrate( data ) {
        // Note that computed properties have not yet rendered
        data.hydrated = true;
    }
    data = {
        "count": 1
    };
    function onHydrateCount( data ) {
        // Note that computed properties have not yet rendered
        data.count += 1;
    }  
    function onUpdate( newValues, oldValues ) {
        // ....
    }
    data = {
        "count": 1
    };
    
    function onUpdateCount( newValue, oldValue ) {
        if ( newValue >= 100 ) {
            // Reset back to 1
            data.count = 1;
        }
    }
    function onUploadError( property, errors, multiple ) {
        // Set error state
        data.uploadFailed = true;
    
        // Create user-friendly error message
        data.errorMessage = "Failed to upload " & ( multiple ? "files" : "file" );
    
        // Log the error for debugging
        if ( !isNull( errors ) ) {
            writeLog( type="error", text="Upload error for #property#: #serializeJSON(errors)#" );
        }
    }
    <!--- ./wires/counter.bxm --->
    <bx:script>
        // @startWire
        data = {
            "counter": 0
        };
        
        function onMount() {
            // Load the counter from the session
            data.counter = session.counter ?: 0;
        }
        
        function save( counter ) {
            // Save the counter to the session
            session.counter = arguments.counter;
        }
        // @endWire
    </bx:script>
    
    <bx:output>
        <div
            x-data="{
                counter: $wire.counter,
                increment() {
                    this.counter++   
                },
                async save() {
                    // Call the save method on our component
                    await $wire.save( this.counter );
                }
            }"
            wire:ignore.self>
            <div>Count: <span x-text="counter"></span></div>
            <button @click="increment">+</button>
            <button @click="save">Save</button>
        </div>
    </bx:output>
    <!--- ./wires/counter.cfm --->
    <cfscript>
        // @startWire
        data = {
            "counter": 0
        };
        
        function onMount() {
            // Load the counter from the session
            data.counter = session.counter ?: 0;
        }
        
        function save( counter ) {
            // Save the counter to the session
            session.counter = arguments.counter;
        }
        // @endWire
    </cfscript>
    
    <cfoutput>
        <div
            x-data="{
                counter: $wire.counter,
                increment() {
                    this.counter++   
                },
                async save() {
                    // Call the save method on our component
                    await $wire.save( this.counter );
                }
            }"
            wire:ignore.self>
            <div>Count: <span x-text="counter"></span></div>
            <button @click="increment">+</button>
            <button @click="save">Save</button>
        </div>
    </cfoutput>
    <!--- ./wires/MyComponent.bxm|cfm --->
    <div>
        <form>
            <input wire:model.live="name" type="text">
            Name updated at #now()#
        </form>
    </div>
    <!-- wires/documentManager.bxm -->
    <bx:output>
    <form wire:submit="save" wire:loading.class="opacity-50">
        <input type="text" wire:model="title" placeholder="Title">
        <textarea wire:model="content" placeholder="Content"></textarea>
        
        <button type="submit" wire:loading.attr="disabled">Save</button>
        <button type="button" wire:click="delete" wire:loading.remove>Delete</button>
        <button type="button" wire:click="reset">Reset</button>
        
        <div wire:loading wire:target="save" wire:loading.delay>Saving document...</div>
        <div wire:loading wire:target="delete">Deleting...</div>
    </form>
    </bx:output>
    <!-- wires/documentManager.cfm -->
    <cfoutput>
    <form wire:submit="save" wire:loading.class="opacity-50">
        <input type="text" wire:model="title" placeholder="Title">
        <textarea wire:model="content" placeholder="Content"></textarea>
        
        <button type="submit" wire:loading.attr="disabled">Save</button>
        <button type="button" wire:click="delete" wire:loading.remove>Delete</button>
        <button type="button" wire:click="reset">Reset</button>
        
        <div wire:loading wire:target="save" wire:loading.delay>Saving document...</div>
        <div wire:loading wire:target="delete">Deleting...</div>
    </form>
    </cfoutput>
    <!-- Target specific action -->
    <div wire:loading wire:target="save">Saving...</div>
    
    <!-- Target multiple actions -->
    <div wire:loading wire:target="save,delete">Processing...</div>
    
    <!-- Target action with parameters -->
    <div wire:loading wire:target="remove(123)">Removing item...</div>
    
    <!-- Exclude specific actions -->
    <div wire:loading wire:target.except="save">Working...</div>
    
    <!-- Target property updates -->
    <input wire:model.live="username">
    <div wire:loading wire:target="username">Checking availability...</div>
    BoxLang Requirements
    • BoxLang 1.0+

    • CBWIRE 5.0+ (BoxLang support was introduced in version 4.1)

    • OpenJDK 21 (required for BoxLang)

    • ColdBox 6+

    • Required modules:

      • - CFML compatibility layer

      • - Security encoding functions

    CFML Requirements

    • Adobe ColdFusion 2023+ or Adobe ColdFusion 2025+

    • Lucee 5.3+ or Lucee 6.0+

    • ColdBox 6+

    Adobe ColdFusion 2018 and 2021 are no longer supported as both have reached end-of-life.

    Installation

    CBWIRE is distributed through ForgeBox and can be installed using CommandBox.

    Install CommandBox

    If you don't have CommandBox installed, download it from ortussolutions.com/products/commandbox.

    Install CBWIRE

    Navigate to your ColdBox application root and run:

    For the latest development version:

    BoxLang Setup

    BoxLang applications require additional configuration to work with CBWIRE. The easiest way to set this up is by creating a server.json file in your project root:

    This configuration automatically:

    • Sets BoxLang as the CFML engine

    • Enables URL rewrites (recommended for ColdBox applications)

    • Uses OpenJDK 21 (required for BoxLang)

    • Installs required compatibility modules on first server start

    Start your server with:

    The compatibility modules are only required for BoxLang applications. CFML applications using Adobe ColdFusion or Lucee don't need these additional modules.

    Asset Integration

    CBWIRE requires CSS and JavaScript assets to function properly. By default, CBWIRE automatically injects these assets into your application's layout, so no manual configuration is needed.

    Automatic Asset Injection (Default)

    When automatic asset injection is enabled (default), CBWIRE will automatically include the necessary CSS in your page's <head> and JavaScript before the closing </body> tag. Your layout will automatically include content similar to this:

    Automatic asset injection requires your layout to have <head></head> and <body></body> tags. CBWIRE uses these tags to inject the necessary CSS and JavaScript. If your layout doesn't include these tags, you'll need to manually place wireStyles() and wireScripts() in your layout. See the Manual Asset Management section below for details.

    Manual Asset Management

    If you prefer to control when and how assets are loaded, you can disable automatic injection:

    Then manually include the assets in your layout:

    Next Steps

    Congratulations! You've successfully set up CBWIRE. Here are some recommended next steps:

    • Learn the Essentials: Explore Components, Templates, and Data Binding

    • Explore Actions: Understand how Actions work and handle user interactions

    • Master Template Directives: Learn about wire:click, wire:model, and other directives

    • Advanced Features: Discover , , and

    Troubleshooting

    If you encounter issues:

    1. Verify assets are loaded: Check your browser's developer tools to ensure CBWIRE CSS and JavaScript are present

    2. Check server logs: Look for any CBWIRE-related errors in your application logs

    3. Validate requirements: Ensure all system requirements are met, especially for BoxLang applications

    4. Review configuration: Double-check your ColdBox configuration if using custom settings

    CBWIRE requires its CSS and JavaScript assets to function. If components aren't responding to interactions, verify that assets are properly loaded in your browser's developer tools.

    Previous versions of CBWIRE required manual asset inclusion. CBWIRE 4+ automatically handles this by default, making setup much simpler.

    What's New With 2.1

    11/26/2022

    New Features

    Direct HTML Rendering

    CBWIRE 2.1 introduces the ability to return HTML directly from the onRender() method, eliminating the need for separate template files for simple components.

    This feature is particularly useful for:

    • Simple notification components

    • Dynamic content that doesn't warrant a separate template file

    • Components with conditional rendering logic

    • Programmatically generated markup

    Bug Fixes

    Computed Properties

    • Fixed empty return values: Computed properties that return no value no longer cause errors

    • Better error handling: Improved error messages when computed properties fail

    Nested Component Rendering

    • Fixed template rendering: Nested components now properly render their individual templates instead of only the last one

    • Fixed component isolation: Nested components no longer interfere with each other's rendering

    • Improved nesting support: Components can now be deeply nested without rendering issues

    Template Data Handling

    • Fixed struct value preservation: Struct values in templates are no longer replaced with empty strings

    • Better data type handling: Improved handling of complex data types in template rendering

    • Preserved variable scope: Template variables maintain their proper types and values

    ColdBox Integration

    • Subdirectory support: CBWIRE now works correctly with ColdBox applications installed in subdirectories

    • Version compatibility: Fixed rendering errors that occurred with the latest ColdBox versions

    • Improved path resolution: Better handling of application paths and module locations

    Component Lifecycle

    • Rendering reliability: Fixed various scenarios where components would fail to render

    • State management: Improved component state handling during complex rendering scenarios

    • Error recovery: Better error handling and recovery during component initialization

    Enhancements

    Performance Improvements

    • Faster rendering for components using onRender() method

    • Reduced overhead for simple components that don't need template files

    • Better memory management for nested component hierarchies

    Developer Experience

    • Clearer error messages for rendering issues

    • Better debugging information for failed component initialization

    • Improved development workflow for simple components

    Template System

    • More reliable template resolution

    • Better handling of edge cases in component nesting

    • Improved variable scope management

    Breaking Changes

    This release maintains full backward compatibility with existing CBWIRE 2.0 applications. No breaking changes were introduced in version 2.1.

    wire:transition

    CBWIRE offers a smooth way to show or hide elements on your webpage with the wire:transition directive. This feature enhances user experience by making elements transition in and out smoothly, rather than just popping into view.

    Basic Usage

    This interactive dashboard component demonstrates multiple wire:transition features including basic transitions, directional control, duration customization, and different effect types:

    // wires/Dashboard.bx
    class extends="cbwire.models.Component" {
        data = {
            "showStats": false,
            "showNotifications": false,
            "showModal": false
        };
    

    What wire:transition Does

    When you add wire:transition to an element, CBWIRE automatically:

    • Fades in/out: Changes opacity from 0 to 100% (and vice versa)

    • Scales: Transforms from slightly smaller to full size by default

    • Smooths interactions: Makes show/hide operations feel polished instead of abrupt

    Available Modifiers

    You can customize transitions with these modifiers:

    • Directional: .in (appear only), .out (disappear only)

    • Duration: .duration.[?ms] (e.g., .duration.300ms)

    • Effects: .opacity

    Combine modifiers as needed: wire:transition.opacity.out.duration.300ms

    Troubleshooting

    Transitions Not Working

    If you're not seeing transition effects, it may be because Livewire is having trouble with DOM diffing. You can often fix this by adding wire:key to the same element that has wire:transition:

    This helps Livewire properly track the element across updates and apply transitions correctly.

    Limitations

    Currently, wire:transition should be applied to a single conditional element and doesn't work as expected with a list of dynamic elements, like individual comments in a loop. This is due to the limitations of how transitions are handled in the underlying framework.

    Use wire:transition for show/hide scenarios on single elements rather than complex list animations for the best results.

    Events

    Events and listeners provide an elegant means for your components to communicate. You can dispatch events from one component, listen for them on another, execute actions, and re-render the listener.

    Dispatching Events

    You can dispatch events directly in your component actions using dispatch().

    // ./wires/Posts.bx
    class extends="cbwire.models.Component" {
        function addPost() {
            dispatch( "post-added" );
        }
    }
    // ./wires/Posts.cfc
    component extends="cbwire.models.Component" {
        function addPost() {
            dispatch( "post-added" );
        }
    }

    You can dispatch events from your templates using $dispatch().

    You can also dispatch using the .

    Passing Parameters

    You can send parameters as your dispatching an event.

    dispatchTo

    You can dispatch events to components of a specific component using dispatchTo().

    dispatchSelf

    You can dispatch events to the component that fired the event using dispatchSelf().

    Listeners

    You register event listeners by adding a listeners struct to your component.

    Listeners are a key/value pair where the key is the event to listen for, and the value is the action to invoke on the component.

    JavaScript keys are case-sensitive. To preserve the key casing in CFML, surround your listener names in quotations.

    Dispatching Browser Events

    You can also dispatch browser events using the dispatch() method.

    You can listen for the event using vanilla JavaScript:

    If you use , you can listen for an event like this.

    Redirecting

    Redirect users to different pages or external URLs from your component actions. CBWIRE provides the redirect() method with options for traditional page redirects or single-page navigation using wire:navigate.

    Basic Usage

    // wires/UserDashboard.bx
    class extends="cbwire.models.Component" {
        data = {
            "isLoggedIn": false
        };
        
        function logout() {
            // Perform logout logic
            data
    

    Redirect Types

    Internal Pages

    Redirect to pages within your application:

    External URLs

    Redirect to external websites:

    Wire Navigate

    Use wire:navigate for SPA-style navigation without full page reloads:

    This creates a faster, single-page application experience by only updating the page content without refreshing the entire browser window.

    Wire navigate (redirect("/path", true)) only works for internal application URLs. External URLs always perform traditional redirects regardless of the second parameter.

    Redirects immediately terminate action execution. Any code after a redirect() call will not execute.

    Configuration

    CBWIRE provides extensive configuration options to customize its behavior for your application needs. You can override the default settings by adding configuration in your ColdBox application's config/ColdBox.bx for BoxLang or config/ColdBox.cfc for CFML.

    All CBWIRE configuration is placed within the moduleSettings struct under the cbwire key. Here's a complete example showing all available configuration options with their default values:

    wire:model

    The wire:model directive creates two-way data binding between form inputs and component data properties. As users type or interact with form elements, the values are automatically synchronized with your server-side data, enabling reactive forms without manual event handling.

    Basic Usage

    This user profile form demonstrates wire:model with various input types and live updating:

    <!-- layouts/Main.bxm -->
    <bx:output>
    <!DOCTYPE html>
    <html>
        <head>
            <!-- CBWIRE CSS -->
            #wireStyles()#
        </head>
        <body>
            <!-- Your application content -->
            
            <!-- CBWIRE JavaScript -->
            #wireScripts()#
        </body>
    </html>
    </bx:output>
    <!-- layouts/Main.cfm -->
    <cfoutput>
    <!DOCTYPE html>
    <html>
        <head>
            <!-- CBWIRE CSS -->
            #wireStyles()#
        </head>
        <body>
            <!-- Your application content -->
            
            <!-- CBWIRE JavaScript -->
            #wireScripts()#
        </body>
    </html>
    </cfoutput>
    box install cbwire@5
    box install cbwire@be
    {
        "app": {
            "cfengine": "boxlang",
            "serverHomeDirectory": ".engine/boxlang"
        },
        "web": {
            "rewrites": {
                "enable": "true"
            }
        },
        "JVM": {
            "javaVersion": "openjdk21_jdk"
        },
        "scripts": {
            "onServerInitialInstall": "install bx-compat-cfml,bx-esapi"
        }
    }
    box start
    <!doctype html>
    <html>
    <head>
        <!-- CBWIRE CSS (automatically injected) -->
        <style>[wire\:loading] { display: none; } /* ... more styles ... */</style>
    </head>
    <body>
        <!-- Your content -->
    
        <!-- CBWIRE JavaScript (automatically injected) -->
        <script src="/modules/cbwire/includes/js/livewire.js" data-csrf="..." data-update-uri="/cbwire/update"></script>
    </body>
    </html>
    // config/ColdBox.bx (BoxLang) or config/ColdBox.cfc (CFML)
    moduleSettings = {
        "cbwire": {
            "autoInjectAssets": false
        }
    };
    // wires/Dashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "showStats" = false,
            "showNotifications" = false,
            "showModal" = false
        };
    
        function toggleStats() {
            data.showStats = !data.showStats;
        }
    
        function toggleNotifications() {
            data.showNotifications = !data.showNotifications;
        }
    
        function showModal() {
            data.showModal = true;
        }
    
        function closeModal() {
            data.showModal = false;
        }
    }
    <!--- ./wires/posts.bxm|cfm --->
    <div>
        <button wire:click="$dispatch( 'post-added' )">Add Post</button>
    </div>
    // wires/UserDashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "isLoggedIn" = false
        };
        
        function logout() {
            // Perform logout logic
            data.isLoggedIn = false;
            
            // Redirect to login page
            redirect("/login");
        }
        
        function goToProfile() {
            // Navigate to profile using wire:navigate (SPA-style)
            redirect("/user/profile", true);
        }
        
        function visitExternal() {
            // Redirect to external URL
            redirect("https://example.com");
        }
    }
    <!-- wires/userDashboard.bxm -->
    <bx:output>
    <div>
        <h1>User Dashboard</h1>
        
        <bx:if isLoggedIn>
            <button wire:click="logout">Logout</button>
            <button wire:click="goToProfile">View Profile</button>
        <bx:else>
            <p>Please log in to continue.</p>
        </bx:if>
        
        <button wire:click="visitExternal">Visit Example Site</button>
    </div>
    </bx:output>
    <!-- wires/userDashboard.cfm -->
    <cfoutput>
    <div>
        <h1>User Dashboard</h1>
        
        <cfif isLoggedIn>
            <button wire:click="logout">Logout</button>
            <button wire:click="goToProfile">View Profile</button>
        <cfelse>
            <p>Please log in to continue.</p>
        </cfif>
        
        <button wire:click="visitExternal">Visit Example Site</button>
    </div>
    </cfoutput>
    bx-compat-cfml
    bx-esapi
    Events
    File Uploads
    Alpine.js Integration
    (fade only),
    .scale
    (scale only)
  • Origins: .origin.top|bottom|left|right (scale origin point)

  • function toggleStats() {
    data.showStats = !data.showStats;
    }
    function toggleNotifications() {
    data.showNotifications = !data.showNotifications;
    }
    function showModal() {
    data.showModal = true;
    }
    function closeModal() {
    data.showModal = false;
    }
    }
    Livewire JavaScript object
    Alpine.js
    .isLoggedIn
    =
    false
    ;
    // Redirect to login page
    redirect("/login");
    }
    function goToProfile() {
    // Navigate to profile using wire:navigate (SPA-style)
    redirect("/user/profile", true);
    }
    function visitExternal() {
    // Redirect to external URL
    redirect("https://example.com");
    }
    }
    Asset Management

    autoInjectAssets

    Controls whether CBWIRE automatically includes its CSS and JavaScript assets in your pages. When enabled, you don't need to manually add wireStyles() in your layout's <head> or wireScripts() before the closing </body> tag.

    Default: true

    When disabled, you'll need to manually include the assets in your layout:

    moduleRootURL

    Specifies the root URL path for CBWIRE module assets and endpoints. Useful when your application is deployed in a subdirectory or when using custom URL mappings.

    Default: /modules/cbwire

    Setting autoInjectAssets to false is useful when you want more control over when and how CBWIRE assets are loaded, or when implementing custom asset optimization strategies.

    Component Configuration

    wiresLocation

    Defines the directory where your CBWIRE components are stored, relative to your application root. This affects component auto-discovery and naming conventions.

    Default: wires

    trimStringValues

    When enabled, automatically trims whitespace from string values in component data properties during updates. This is particularly useful for form inputs where users might accidentally include leading or trailing spaces.

    Default: false

    You can also enable this setting on individual components:

    Changing the wiresLocation after components have been created will require updating your component references and possibly your routing configuration.

    throwOnMissingSetterMethod

    Controls whether CBWIRE throws an exception when trying to set a property that doesn't have a corresponding setter method. When disabled, missing setters are silently ignored.

    Default: false

    Request Handling

    updateEndpoint

    Sets the URI endpoint where CBWIRE processes component updates and actions. You may need to modify this if URL rewriting is disabled or if you're using custom routing.

    Default: /cbwire/updates

    maxUploadSeconds

    Specifies the maximum time (in seconds) allowed for file upload operations to complete. This prevents long-running uploads from consuming server resources indefinitely.

    Default: 300 (5 minutes)

    uploadsStoragePath

    Configures a custom directory path for temporary file uploads. This is particularly useful in distributed server environments where you need to share temporary files across multiple front-end servers.

    Default: getTempDirectory() & "/cbwire" (system temporary directory)

    When set, CBWIRE will use this directory instead of the system's temporary directory for uploaded files. This enables scenarios such as:

    • Sharing temporary uploads across clustered servers using a network-mounted directory

    • Using a specific disk volume optimized for temporary file operations

    • Implementing custom cleanup or monitoring policies on temporary file storage

    See the File Uploads documentation for details on how uploaded files are stored and managed.

    Ensure the configured path exists and has appropriate read/write permissions for your application server.

    storagePath

    Configures a custom directory path for single-file component compilation. When CBWIRE compiles single-file components (.bxm files with embedded code), it stores the compiled component classes in this directory.

    Default: {module}/models/tmp (CBWIRE module's internal directory)

    The storagePath must be within a directory accessible to WireBox for component instantiation. Unlike uploadsStoragePath which can use any directory, this path requires proper classpath configuration.

    When URL rewriting is disabled, remember to include /index.cfm in your updateEndpoint configuration.

    UI Features

    showProgressBar

    Controls whether a progress bar appears at the top of the page during wire:navigate operations. The progress bar provides visual feedback during page transitions.

    Default: true

    progressBarColor

    Customizes the color of the progress bar displayed during wire:navigate operations. Accepts any valid CSS color value.

    Default: #2299dd

    The progress bar only appears when using wire:navigate for page transitions. Standard CBWIRE component updates don't trigger the progress bar.

    Security Configuration

    csrfEnabled

    Enables Cross-Site Request Forgery (CSRF) protection for CBWIRE requests. When enabled, all component actions require a valid CSRF token. CSRF protection is enabled by default in CBWIRE 5.x.

    Default: true

    csrfStorage

    Specifies the storage implementation used to store CSRF tokens. CBWIRE provides two built-in implementations: SessionCSRFStorage@cbwire (default, recommended) for session-based storage, and CacheCSRFStorage@cbwire for cache-based storage in distributed environments.

    Default: SessionCSRFStorage@cbwire

    See the Security documentation for complete details on CSRF protection, including custom storage implementations.

    Session-based CSRF storage (default) follows OWASP recommendations and eliminates "Page Expired" errors common with cache-based approaches.

    checksumValidation

    Enables or disables checksum validation for component payloads. This security feature validates the integrity of data sent between the client and server to prevent tampering.

    Default: true

    We recommend always leaving checksum validation enabled for security, but you can disable it as needed for debugging or specific use cases.

    All configuration options are optional. CBWIRE will use sensible defaults if no custom configuration is provided.

    // config/ColdBox.bx
    class {
        function configure() {
            moduleSettings = {
                "cbwire": {
                    // Asset Management
                    "autoInjectAssets": true,
                    "moduleRootURL": "/modules/cbwire",
                    
                    // Component Configuration
                    "wiresLocation": "wires",
                    "trimStringValues": false,
                    "throwOnMissingSetterMethod": false,
                    
                    // Request Handling
                    "updateEndpoint": "/cbwire/update",
                    "maxUploadSeconds": 300, // 5 minutes
                    "uploadsStoragePath": "", // Custom temporary storage path for file uploads
                    "storagePath": "", // Custom storage path for component compilation
                    
                    // UI Features
                    "showProgressBar": true,
                    "progressBarColor": "#2299dd",
                    
                    // Security
                    "csrfEnabled": true,
                    "csrfStorage": "SessionCSRFStorage@cbwire",
                    "checksumValidation": true
                }
            };
        }
    }

    What wire:model Does

    When you add wire:model to an input, CBWIRE automatically:

    • Binds Data: Creates two-way binding between input values and component properties

    • Syncs Changes: Updates server data when users interact with form elements

    • Preserves State: Maintains input values across component re-renders

    • Handles Types: Automatically manages different input types (text, checkbox, select, etc.)

    Available Modifiers

    The wire:model directive supports these modifiers:

    • Live: .live - Sends updates as user types (with 150ms debounce)

    • Blur: .blur - Only sends updates when input loses focus

    • Change: .change - Only sends updates on change event

    • Lazy: .lazy - Prevents server updates until an action is called

    • Debounce: .debounce.Xms - Customizes debounce timing (e.g., .debounce.500ms)

    • Throttle: .throttle.Xms - Throttles network requests by X milliseconds

    • Number: .number - Casts text input to integer on server

    • Boolean: .boolean - Casts text input to boolean on server

    • Fill: .fill - Uses initial HTML value attribute during page load

    Combine modifiers as needed: wire:model.live.debounce.500ms="username"

    Input Types

    Text Inputs

    Textarea

    Don't initialize textarea content with the property value in the template:

    Single Checkbox

    The checkbox will be checked if the data property is true, unchecked if false.

    Multiple Checkboxes

    Radio Buttons

    Select Dropdowns

    Livewire automatically adds the selected attribute to the matching option.

    Dynamic Select Options

    Dependent Dropdowns

    Use wire:key to ensure dependent dropdowns update properly:

    Multi-Select Dropdowns

    By default, wire:model only sends updates when actions are performed. Use wire:model.live for real-time validation or immediate server updates as users type.

    // wires/UserProfile.bx
    class extends="cbwire.models.Component" {
        data = {
            "name": "",
            "email": "",
            "bio": "",
            "notifications": true,
            "preferences": [],
            "country": ""
        };
    
        function save() {
            // Save user profile
            sleep(1000);
        }
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        data = {
            "name" = "",
            "email" = "",
            "bio" = "",
            "notifications" = true,
            "preferences" = [],
            "country" = ""
        };
    
        function save() {
            // Save user profile
            sleep(1000);
        }
    }
    // wires/SimpleAlert.cfc
    component extends="cbwire.models.Component" {
        data = {
            "message" = "",
            "type" = "info",
            "visible" = true
        };
        
        function mount(params = {}) {
            data.message = params.message ?: "Default message";
            data.type = params.type ?: "info";
        }
        
        function onRender() {
            // Return HTML directly without needing a .cfm file
            if (!data.visible) {
                return "";
            }
            
            var alertClass = "alert alert-" & data.type;
            var html = '
            <div class="#alertClass#">
                <button wire:click="dismiss" class="close">&times;</button>
                #data.message#
            </div>';
            
            return html;
        }
        
        function dismiss() {
            data.visible = false;
        }
    }
    // wires/DynamicCard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "items" = [],
            "cardType" = "default"
        };
        
        function onRender() {
            var html = '<div class="card card-#data.cardType#">';
            
            for (var item in data.items) {
                html &= '<div class="card-item">';
                html &= '<h4>#item.title#</h4>';
                html &= '<p>#item.description#</p>';
                html &= '</div>';
            }
            
            html &= '</div>';
            return html;
        }
        
        function addItem(title, description) {
            arrayAppend(data.items, {
                "title" = title,
                "description" = description
            });
        }
    }
    // This now works correctly in 2.1
    component extends="cbwire.models.Component" {
        function getDisplayName() {
            // This won't error if it returns nothing
            if (structKeyExists(data, "name")) {
                return data.name;
            }
            // Implicit return of empty value handled gracefully
        }
    }
    <!-- This now works correctly -->
    <cfoutput>
    <div class="parent-component">
        #wire("ChildComponent1")#
        #wire("ChildComponent2")# 
        #wire("NestedContainer")#
    </div>
    </cfoutput>
    <!-- wires/dashboard.bxm -->
    <bx:output>
    <div class="dashboard">
        <h1>Dashboard</h1>
        
        <div class="controls">
            <button wire:click="toggleStats">
                <bx:if showStats>Hide<bx:else>Show</bx:if> Stats
            </button>
            <button wire:click="toggleNotifications">Notifications</button>
            <button wire:click="showModal">Open Modal</button>
        </div>
    
        <!-- Basic transition with default fade and scale -->
        <bx:if showStats>
            <div wire:transition class="stats-panel">
                <h3>Statistics</h3>
                <p>Users: 1,234 | Sales: $56,789</p>
            </div>
        </bx:if>
    
        <!-- Opacity-only transition with custom duration -->
        <bx:if showNotifications>
            <div wire:transition.opacity.duration.300ms class="notifications">
                <h3>Recent Notifications</h3>
                <div class="notification">New user registered</div>
                <div class="notification">Payment received</div>
            </div>
        </bx:if>
    
        <!-- Modal with scale transition from top -->
        <bx:if showModal>
            <div class="modal-backdrop">
                <div wire:transition.scale.origin.top.duration.250ms class="modal">
                    <h3>Confirm Action</h3>
                    <p>Are you sure you want to proceed?</p>
                    <button wire:click="closeModal">Cancel</button>
                    <button wire:click="closeModal">Confirm</button>
                </div>
            </div>
        </bx:if>
    </div>
    </bx:output>
    <!-- wires/dashboard.cfm -->
    <cfoutput>
    <div class="dashboard">
        <h1>Dashboard</h1>
        
        <div class="controls">
            <button wire:click="toggleStats">
                <cfif showStats>Hide<cfelse>Show</cfif> Stats
            </button>
            <button wire:click="toggleNotifications">Notifications</button>
            <button wire:click="showModal">Open Modal</button>
        </div>
    
        <!-- Basic transition with default fade and scale -->
        <cfif showStats>
            <div wire:transition class="stats-panel">
                <h3>Statistics</h3>
                <p>Users: 1,234 | Sales: $56,789</p>
            </div>
        </cfif>
    
        <!-- Opacity-only transition with custom duration -->
        <cfif showNotifications>
            <div wire:transition.opacity.duration.300ms class="notifications">
                <h3>Recent Notifications</h3>
                <div class="notification">New user registered</div>
                <div class="notification">Payment received</div>
            </div>
        </cfif>
    
        <!-- Modal with scale transition from top -->
        <cfif showModal>
            <div class="modal-backdrop">
                <div wire:transition.scale.origin.top.duration.250ms class="modal">
                    <h3>Confirm Action</h3>
                    <p>Are you sure you want to proceed?</p>
                    <button wire:click="closeModal">Cancel</button>
                    <button wire:click="closeModal">Confirm</button>
                </div>
            </div>
        </cfif>
    </div>
    </cfoutput>
    <!-- If this doesn't transition properly -->
    <bx:if showPanel>
        <div wire:transition>
            Panel content
        </div>
    </bx:if>
    
    <!-- Try adding wire:key -->
    <bx:if showPanel>
        <div wire:transition wire:key="panel-content">
            Panel content
        </div>
    </bx:if>
    <!-- If this doesn't transition properly -->
    <cfif showPanel>
        <div wire:transition>
            Panel content
        </div>
    </cfif>
    
    <!-- Try adding wire:key -->
    <cfif showPanel>
        <div wire:transition wire:key="panel-content">
            Panel content
        </div>
    </cfif>
    <!-- ✅ Works well -->
    <bx:if showPanel>
        <div wire:transition>Panel content</div>
    </bx:if>
    
    <!-- ❌ Not recommended -->
    <bx:loop array="#items#" index="item">
        <div wire:transition>Item content</div>
    </bx:loop>
    <!-- ✅ Works well -->
    <cfif showPanel>
        <div wire:transition>Panel content</div>
    </cfif>
    
    <!-- ❌ Not recommended -->
    <cfloop array="#items#" index="item">
        <div wire:transition>Item content</div>
    </cfloop>
    <!--- ./wires/posts.bxm|cfm --->
    <script>
        Livewire.dispatch( 'post-added' );
    </script>
    function addPost() {
        dispatch( "post-added", { "id": post.getId() } );
    }
    <button wire:click="$dispatch( 'post-added', { id: post.getId() } )">Add Post</button>
    <script>
        Livewire.dispatch( 'post-added', { id: '#post.getID()#' } );
    </script>
    function addPost() {
        dispatchTo( "TopBar", "post-added", {} );
    }
    <button wire:click="$dispatchTo( 'TopBar', 'post-added', {} )">Add Post</button>
    <script>
        Livewire.dispatchTo( 'TopBar', 'post-added', { id: '#post.getID()#' } );
    </script>
    function addPost() {
        dispatchSelf( "post-added", { "id": post.getId() } );
    }
    <button wire:click="$dispatchSelf( 'post-added', { id: post.getId() } )">Add Post</button>
    // ./wires/Posts.bx
    class extends="cbwire.models.Component" {
        listeners = {
            "post-added": "incrementPostCount" 
        };   
        function incrementPostCount() {
            // Increment the post count
        }
    }
    // ./wires/Posts.cfc
    component extends="cbwire.models.Component" {
        listeners = {
            "post-added": "incrementPostCount" 
        };   
        function incrementPostCount() {
            // Increment the post count
        }
    }
    function submitForm(){
        // Dispatch a 'form-submitted' browser event. Pass parameters
        dispatch( "form-submitted", { "submittedBy": "Grant" } );
    }
    <div>
        <form wire:submit.prevent="submitForm">
            <button type="submit">Dispatch event</button>
        </form>
    </div>
    <script>
        window.addEventListener('form-submitted', event => {
            alert( 'Form submitted by: ' + event.detail.submittedBy );
        } );
    </script>
    <div x-data="{ show: false }" @form-submitted.window="show = true">
        <div x-show="true">
            Form was submitted.
        </div>
    </div>
    redirect("/dashboard");
    redirect("/users/123");
    redirect("/admin/settings");
    redirect("https://google.com");
    redirect("https://github.com/user/repo");
    redirect("/dashboard", true);
    // wires/ContactForm.bx
    class extends="cbwire.models.Component" {
        trimStringValues = true;
        
        data = {
            "name": "",
            "email": "",
            "message": ""
        };
    }
    // wires/ContactForm.cfc
    component extends="cbwire.models.Component" {
        trimStringValues = true;
        
        data = {
            "name" = "",
            "email" = "",
            "message" = ""
        };
    }
    // config/ColdBox.bx
    moduleSettings = {
        "cbwire": {
            "uploadsStoragePath": "/shared/temp/uploads"
        }
    };
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "uploadsStoragePath" = "/shared/temp/uploads"
        }
    };
    // config/ColdBox.bx
    moduleSettings = {
        "cbwire": {
            "storagePath": "/custom/component/compilation"
        }
    };
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "storagePath" = "/custom/component/compilation"
        }
    };
    // config/ColdBox.cfc
    component {
        function configure() {
            moduleSettings = {
                "cbwire" = {
                    // Asset Management
                    "autoInjectAssets" = true,
                    "moduleRootURL" = "/modules/cbwire",
                    
                    // Component Configuration
                    "wiresLocation" = "wires",
                    "trimStringValues" = false,
                    "throwOnMissingSetterMethod" = false,
                    
                    // Request Handling
                    "updateEndpoint" = "/cbwire/updates",
                    "maxUploadSeconds" = 300, // 5 minutes
                    "uploadsStoragePath" = "", // Custom temporary storage path for file uploads
                    "storagePath" = "", // Custom storage path for component compilation
                    
                    // UI Features
                    "showProgressBar" = true,
                    "progressBarColor" = "##2299dd",
                    
                    // Security
                    "csrfEnabled" = true,
                    "csrfStorage" = "SessionCSRFStorage@cbwire",
                    "checksumValidation" = true
                }
            };
        }
    }
    moduleSettings = {
        "cbwire": {
            "autoInjectAssets": false
        }
    };
    <!-- In your layout's <head> -->
    #wireStyles()#
    
    <!-- Before closing </body> -->
    #wireScripts()#
    moduleSettings = {
        "cbwire": {
            "wiresLocation": "app/components"
        }
    };
    moduleSettings = {
        "cbwire": {
            "updateEndpoint": "/index.cfm/cbwire/updates"
        }
    };
    moduleSettings = {
        "cbwire": {
            "maxUploadSeconds": 600 // 10 minutes
        }
    };
    moduleSettings = {
        "cbwire": {
            "showProgressBar": false
        }
    };
    moduleSettings = {
        "cbwire": {
            "progressBarColor": "#ff6b35"
        }
    };
    moduleSettings = {
        "cbwire": {
            "csrfEnabled": false // Disable if needed (not recommended)
        }
    };
    moduleSettings = {
        "cbwire": {
            // Use cache storage for distributed/clustered deployments
            "csrfStorage": "CacheCSRFStorage@cbwire"
        }
    };
    moduleSettings = {
        "cbwire": {
            "checksumValidation": false
        }
    };
    <!-- wires/userProfile.bxm -->
    <bx:output>
    <form wire:submit="save">
        <input type="text" wire:model.live.debounce.300ms="name" placeholder="Name">
        <input type="email" wire:model.blur="email" placeholder="Email">
        <textarea wire:model="bio" placeholder="Bio"></textarea>
        
        <input type="checkbox" wire:model="notifications"> Email notifications
        
        <input type="checkbox" value="updates" wire:model="preferences"> Product updates
        <input type="checkbox" value="marketing" wire:model="preferences"> Marketing
        
        <select wire:model="country">
            <option value="">Select country</option>
            <option value="US">United States</option>
            <option value="CA">Canada</option>
        </select>
        
        <button type="submit">Save Profile</button>
    </form>
    </bx:output>
    <!-- wires/userProfile.cfm -->
    <cfoutput>
    <form wire:submit="save">
        <input type="text" wire:model.live.debounce.300ms="name" placeholder="Name">
        <input type="email" wire:model.blur="email" placeholder="Email">
        <textarea wire:model="bio" placeholder="Bio"></textarea>
        
        <input type="checkbox" wire:model="notifications"> Email notifications
        
        <input type="checkbox" value="updates" wire:model="preferences"> Product updates
        <input type="checkbox" value="marketing" wire:model="preferences"> Marketing
        
        <select wire:model="country">
            <option value="">Select country</option>
            <option value="US">United States</option>
            <option value="CA">Canada</option>
        </select>
        
        <button type="submit">Save Profile</button>
    </form>
    </cfoutput>
    <!-- ❌ Don't do this -->
    <textarea wire:model="content">#content#</textarea>
    
    <!-- ✅ Do this instead -->
    <textarea wire:model="content"></textarea>
    // Component data
    data = {
        "selections": []
    };
    // Component data
    data = {
        "selections" = []
    };
    function getStates() {
        return queryExecute("select id, label from states");
    }
    function getStates() {
        return queryExecute("select id, label from states");
    }
    <select wire:model="state">
        <option disabled value="">Select a state</option>
        <bx:loop array="#getStates()#" index="state">
            <option value="#state.id#">#state.label#</option>
        </bx:loop>
    </select>
    <select wire:model="state">
        <option disabled value="">Select a state</option>
        <cfloop array="#getStates()#" index="state">
            <option value="#state.id#">#state.label#</option>
        </cfloop>
    </select>
    <!-- State selector -->
    <select wire:model.live="state">
        <option disabled value="">Select a state</option>
        <bx:loop array="#getStates()#" index="state">
            <option value="#state.id#">#state.label#</option>
        </bx:loop>
    </select>
    
    <!-- City selector (depends on state) -->
    <select wire:model.live="city" wire:key="#state#">
        <option disabled value="">Select a city</option>
        <bx:loop array="#getCities(state)#" index="city">
            <option value="#city.id#">#city.label#</option>
        </bx:loop>
    </select>
    <!-- State selector -->
    <select wire:model.live="state">
        <option disabled value="">Select a state</option>
        <cfloop array="#getStates()#" index="state">
            <option value="#state.id#">#state.label#</option>
        </cfloop>
    </select>
    
    <!-- City selector (depends on state) -->
    <select wire:model.live="city" wire:key="#state#">
        <option disabled value="">Select a city</option>
        <cfloop array="#getCities(state)#" index="city">
            <option value="#city.id#">#city.label#</option>
        </cfloop>
    </select>
    <input type="text" wire:model="name">
    <input type="email" wire:model="email">
    <input type="password" wire:model="password">
    <textarea wire:model="content"></textarea>
    <input type="checkbox" wire:model="receiveUpdates">
    <input type="checkbox" value="email" wire:model="selections">
    <input type="checkbox" value="sms" wire:model="selections">
    <input type="checkbox" value="notification" wire:model="selections">
    <input type="radio" value="yes" wire:model="sendEmail">
    <input type="radio" value="no" wire:model="sendEmail">
    <select wire:model="state">
        <option value="AL">Alabama</option>
        <option value="AK">Alaska</option>
        <option value="AZ">Arizona</option>
    </select>
    <select wire:model="states" multiple>
        <option value="AL">Alabama</option>
        <option value="AK">Alaska</option>
        <option value="AZ">Arizona</option>
    </select>

    What's New With 4.0

    05/16/2024

    New Features

    Livewire.js 3.0 Upgrade

    CBWIRE 4.0 features a complete upgrade to Livewire.js version 3, bringing modern frontend capabilities and improved performance. This major upgrade provides the foundation for all new features in this release.

    Alpine.js Integration

    Alpine.js is now automatically included with CBWIRE, eliminating the need for separate installation and configuration. This seamless integration provides enhanced frontend interactivity out of the box.

    Lazy Loading Components

    Improve application performance with lazy loading capabilities. Components can now be loaded on-demand, reducing initial page load times. While the component loads, a placeholder is displayed to provide visual feedback to users.

    Components can define a placeholder() method that returns HTML content to display while the component is loading. This provides a better user experience during the loading process.

    JavaScript Execution from Actions

    Execute JavaScript directly from your component actions using the new js() method, enabling seamless server-to-client communication.

    Single-Page Applications with wire:navigate

    Transform your application into a single-page application using the new wire:navigate directive for seamless navigation without full page reloads.

    Content Streaming with wire:stream

    Stream content in real-time using the new wire:stream directive for dynamic content updates.

    Smooth Transitions with wire:transition

    Add smooth animations and transitions to your UI updates using the wire:transition directive. This feature enhances user experience by making elements transition in and out smoothly, rather than just popping into view.

    Available modifiers include:

    • Directional: .in (appear only), .out (disappear only)

    • Duration: .duration.[?ms] (e.g., .duration.300ms)

    • Effects: .opacity

    Request Bundling

    Optimize performance with intelligent request bundling that reduces the number of server requests by combining multiple component updates into single requests.

    Complete Engine Rewrite

    CBWIRE 4.0 features a completely rewritten engine architecture providing:

    • Improved performance and reliability

    • Better component lifecycle management

    • Enhanced debugging capabilities

    • Modern development patterns

    Enhancements

    Lifecycle Method Improvements

    Updated lifecycle methods for better consistency and clarity:

    • mount() renamed to onMount()

    • updated() renamed to onUpdate()

    Configuration Cleanup

    Streamlined configuration by removing deprecated settings:

    • Removed enableTurbo global setting (superseded by wire:navigate)

    • Removed throwOnMissingSetter global setting (improved error handling)

    Performance Optimizations

    • Faster component rendering and updates

    • Optimized request handling with bundling

    • Improved memory usage and garbage collection

    • Enhanced component lifecycle management

    Breaking Changes

    Lifecycle Methods

    If you're upgrading from CBWIRE 3.x, update your lifecycle methods:

    Configuration Settings

    Remove deprecated settings from your configuration:

    WireBox

    CBWIRE seamlessly integrates with WireBox, ColdBox's powerful dependency injection framework, allowing you to inject services, models, and other dependencies directly into your components. This separation of concerns keeps business logic out of your components and promotes clean, maintainable code architecture.

    Basic Dependency Injection

    The most common way to access dependencies is through property injection using the inject attribute:

    // models/UserService.bx
    class singleton {
        function getAll() {
            return queryExecute("select id, name, email from users");
        }
        
        function create(userData) {
            return 
    

    Dynamic Instance Retrieval

    For cases where you need to retrieve dependencies dynamically within action methods, use the getInstance() method:

    Common Injection Patterns

    Service Layer Injection

    LogBox Logger Injection

    ColdBox Settings Injection

    Custom Provider Injection

    getInstance() Method Signature

    The getInstance() method provides flexible dependency retrieval:

    Usage Examples

    Lifecycle Integration

    CBWIRE components fully participate in WireBox's lifecycle management. All WireBox lifecycle methods are available:

    Benefits of WireBox Integration

    • Separation of Concerns: Keep business logic in dedicated service layers

    • Testability: Easily mock dependencies for unit testing

    • Flexibility: Switch implementations via WireBox mappings

    • Performance: Leverage WireBox's singleton and caching capabilities

    Using WireBox with CBWIRE promotes clean architecture by separating presentation logic (components) from business logic (services), making your application more maintainable and testable.

    CBWIRE uses WireBox internally to instantiate your components, so all WireBox lifecycle methods and features are automatically available to your components.

    Further Documentation

    Learn about the full capabilities of WireBox at .

    wire:navigate

    The wire:navigate directive provides SPA-like page navigation by intercepting link clicks and loading pages in the background. Instead of full page refreshes, Livewire fetches new content and swaps it seamlessly, resulting in faster navigation and a smoother user experience.

    Basic Usage

    This navigation example demonstrates wire:navigate with hover prefetching and redirect functionality:

    // wires/BlogPost.bx
    class extends="cbwire.models.Component" {
        data = {
            "title": "",
            "content": ""
        };
    
        function save() {
    
    // wires/BlogPost.cfc
    component extends="cbwire.models.Component" {
        data = {
            "title" = "",
            "content" = ""
        };
    
        function save() {
            // Save blog post
            redirect(
    

    What wire:navigate Does

    When you add wire:navigate to a link, CBWIRE automatically:

    • Intercepts Clicks: Prevents browser from performing full page visits

    • Background Fetching: Requests new page content via AJAX with loading indicator

    • Seamless Swapping: Replaces URL, <title>, and <body> content without refresh

    Available Modifiers

    The wire:navigate directive supports one modifier:

    • Hover: .hover - Prefetches page content when user hovers over link for 60ms

    Example: wire:navigate.hover improves perceived performance but increases server usage.

    Navigation Process

    Here's what happens during navigation:

    1. User clicks a wire:navigate link

    2. Livewire prevents default browser navigation

    3. Loading bar appears at top of page

    4. Page content is fetched in background

    Redirecting with Navigation

    Use CBWIRE's redirect() method with navigation enabled:

    Persisting Elements Across Pages

    Use Alpine's x-persist to maintain elements like audio/video players:

    Preserving Scroll Position

    Livewire preserves page scroll by default. For specific elements, use wire:scroll:

    JavaScript Hooks

    Navigation triggers three lifecycle events:

    Manual Navigation

    Trigger navigation programmatically:

    Event Listeners

    Handle persistent event listeners properly:

    Analytics Integration

    Ensure analytics track navigation properly:

    Script Handling

    Control script execution across navigations:

    Progress Bar Customization

    Configure the loading indicator:

    Custom Loader Example

    Create a custom progress indicator:

    wire:navigate often provides 2x faster page loads and creates a JavaScript SPA-like experience while maintaining server-side rendering benefits.

    Prefetching with .hover increases server usage. Use judiciously on high-traffic sites.

    Persisted elements must be placed outside CBWIRE components, typically in your main layout. Use livewire:navigated instead of DOMContentLoaded for page-specific code.

    What's New With 2.0

    08/30/2022

    New Features

    File Upload Support

    CBWIRE 2.0 introduces comprehensive file upload capabilities with built-in handling for single and multiple files.

    // models/UserService.cfc
    component singleton {
        function getAll() {
            return queryExecute("select id, name, email from users");
        }
        
        function create(userData) {
            return queryExecute(
                "insert into users (name, email) values (:name, :email)",
                {
                    name: {value: userData.name, cfsqltype: "cf_sql_varchar"},
                    email: {value: userData.email, cfsqltype: "cf_sql_varchar"}
                }
            );
        }
        
        function delete(userId) {
            queryExecute(
                "delete from users where id = :id", 
                {id: {value: userId, cfsqltype: "cf_sql_integer"}}
            );
        }
    }
    // wires/UserManager.bx
    class extends="cbwire.models.Component" {
        // Inject dependencies via properties
        property name="userService" inject="UserService";
        property name="logger" inject="logbox:logger:{this}";
        
        data = {
            "users": [],
            "newUser": {"name": "", "email": ""}
        };
    
        function onMount() {
            data.users = userService.getAll();
            logger.info("UserManager component mounted");
        }
    
        function addUser() {
            if (data.newUser.name.len() && data.newUser.email.len()) {
                userService.create(data.newUser);
                data.users = userService.getAll(); // Refresh list
                data.newUser = {"name": "", "email": ""}; // Reset form
                logger.info("User created: #data.newUser.name#");
            }
        }
    
        function deleteUser(userId) {
            userService.delete(userId);
            data.users = userService.getAll(); // Refresh list
            logger.info("User deleted: #userId#");
        }
    }
    (fade only),
    .scale
    (scale only)
  • Origins: .origin.top|bottom|left|right (scale origin point)

  • // wires/InteractiveComponent.bx
    class extends="cbwire.models.Component" {
        data = {
            "showDetails": false,
            "message": "Click to reveal"
        };
        
        function toggleDetails() {
            data.showDetails = !data.showDetails;
        }
    }
    // wires/InteractiveComponent.cfc
    component extends="cbwire.models.Component" {
        data = {
            "showDetails" = false,
            "message" = "Click to reveal"
        };
        
        function toggleDetails() {
            data.showDetails = !data.showDetails;
        }
    }

    Configuration: Externalize configuration through WireBox DSL

    queryExecute
    (
    "insert into users (name, email) values (:name, :email)",
    {
    name: {value: userData.name, cfsqltype: "cf_sql_varchar"},
    email: {value: userData.email, cfsqltype: "cf_sql_varchar"}
    }
    );
    }
    function delete(userId) {
    queryExecute(
    "delete from users where id = :id",
    {id: {value: userId, cfsqltype: "cf_sql_integer"}}
    );
    }
    }
    https://wirebox.ortusbooks.com/

    Performance Boost: Often achieves 2x faster page loads than traditional navigation

    New content replaces current page seamlessly

    // Save blog post
    redirect("/blog", true); // Navigate using wire:navigate
    }
    function cancel() {
    redirect("/blog", true);
    }
    }
    "/blog"
    ,
    true);
    // Navigate using wire:navigate
    }
    function cancel() {
    redirect("/blog", true);
    }
    }
    Component Testing

    Write comprehensive unit tests for your CBWIRE components with the new testing framework.

    Custom Wires Directory

    Override the default wires folder location to organize components according to your application structure.

    Skip Rendering with noRender()

    Prevent template rendering when you only need to update data without returning HTML.

    Dirty Property Tracking

    Track which properties have changed since the last render for optimized updates.

    Computed Property Optimization

    Computed properties now run only once per request during rendering for better performance.

    Dependency Injection Support

    Components now support ColdBox's dependency injection system.

    Enhanced Turbo Support

    Enable single-page application functionality with improved Turbo integration.

    Enhancements

    Template Path Flexibility

    Set custom template paths for components using the this.template property.

    Null Value Support

    Data properties now properly support null as a valid value.

    Livewire JavaScript Upgrade

    Updated to Livewire JS v2.10.6 for improved client-side functionality and bug fixes.

    Performance Optimizations

    • Disabled browser caching for XHR responses to ensure fresh data

    • Trimmed XHR payload by removing unnecessary data

    • Moved internal methods to Engine object to prevent naming conflicts

    • Added security by rejecting XHR requests without proper headers

    Bug Fixes

    Component State Management

    • Fixed reset() errors: Calling reset("someProperty") no longer causes errors

    • Preserved component IDs: Component IDs are now preserved across renders to avoid DOM diffing issues

    • Better parameter handling: Fixed missing parameters in update method calls

    Browser Compatibility

    • Fixed back button issues: Browser back button now works correctly with CBWIRE components

    • Improved navigation: Better handling of browser history and navigation states

    Data Integrity

    • Parameter validation: Ensured params is properly passed as an array

    • Method parameter handling: Fixed missing parameters in component method calls

    Breaking Changes

    This is a major version release with some breaking changes:

    Component Structure

    Some internal method names have changed to prevent conflicts. If you were extending or overriding internal CBWIRE methods, you may need to update your code.

    XHR Security

    XHR requests now require the X-Livewire header. This improves security but may affect custom AJAX implementations.

    Template Resolution

    The default template resolution has been improved. If you have custom template path logic, verify it still works correctly with the new resolution system.

    <!-- Template with Alpine.js integration -->
    <div x-data="{ expanded: $wire.showDetails }">
        <button wire:click="toggleDetails" @click="expanded = !expanded">
            #message#
        </button>
        <div x-show="expanded" x-transition>
            <p>Additional details revealed!</p>
        </div>
    </div>
    <!-- Lazy load a component using wire() with lazy=true -->
    #wire("Dashboard", {}, "", true)#
    
    <!-- Lazy load with parameters -->
    #wire("UserProfile", { "userId": 123 }, "", true)#
    
    <!-- Lazy load with key and parameters -->
    #wire("UserProfile", { "userId": 123 }, "user-profile-key", true)#
    
    <!-- Lazy load with isolated set to false -->
    #wire("SharedComponent", {}, "", true, false)#
    // wires/LazyDashboard.bx
    class extends="cbwire.models.Component" {
        function placeholder() {
            return "<div>Loading dashboard...</div>";
        }
        
        data = {
            "metrics": [],
            "loaded": false
        };
        
        function onMount() {
            // Simulate loading data
            sleep(1000);
            data.metrics = getMetricsData();
            data.loaded = true;
        }
    }
    // wires/LazyDashboard.cfc
    component extends="cbwire.models.Component" {
        function placeholder() {
            return "<div>Loading dashboard...</div>";
        }
        
        data = {
            "metrics" = [],
            "loaded" = false
        };
        
        function onMount() {
            // Simulate loading data
            sleep(1000);
            data.metrics = getMetricsData();
            data.loaded = true;
        }
    }
    // wires/NotificationComponent.bx
    class extends="cbwire.models.Component" {
        function showAlert() {
            return js("alert('Action completed successfully!')");
        }
        
        function updateTitle() {
            return js("document.title = 'Updated by CBWIRE'");
        }
        
        function focusInput() {
            return js("document.getElementById('username').focus()");
        }
    }
    // wires/NotificationComponent.cfc
    component extends="cbwire.models.Component" {
        function showAlert() {
            return js("alert('Action completed successfully!')");
        }
        
        function updateTitle() {
            return js("document.title = 'Updated by CBWIRE'");
        }
        
        function focusInput() {
            return js("document.getElementById('username').focus()");
        }
    }
    <!-- Traditional links become SPA navigation -->
    <a href="/dashboard" wire:navigate>Dashboard</a>
    <a href="/profile" wire:navigate>Profile</a>
    
    <!-- Works with forms and redirects -->
    <form wire:submit="updateProfile">
        <input wire:model="name" placeholder="Name">
        <button type="submit">Update Profile</button>
    </form>
    // wires/ProfileComponent.bx
    class extends="cbwire.models.Component" {
        function updateProfile() {
            // Save profile logic
            return redirect("/profile", true); // true enables wire:navigate
        }
    }
    // wires/ProfileComponent.cfc
    component extends="cbwire.models.Component" {
        function updateProfile() {
            // Save profile logic
            return redirect("/profile", true); // true enables wire:navigate
        }
    }
    <!-- Stream live updates -->
    <div wire:stream="notifications">
        <bx:loop array="#notifications#" item="notification">
            <div class="notification">#notification.message#</div>
        </bx:loop>
    </div>
    // wires/LiveFeed.bx
    class extends="cbwire.models.Component" {
        data = {
            "notifications": []
        };
        
        function addNotification(message) {
            data.notifications.append({
                "id": createUUID(),
                "message": message,
                "timestamp": now()
            });
            
            stream("notifications");
        }
    }
    // wires/LiveFeed.cfc
    component extends="cbwire.models.Component" {
        data = {
            "notifications" = []
        };
        
        function addNotification(message) {
            arrayAppend(data.notifications, {
                "id" = createUUID(),
                "message" = message,
                "timestamp" = now()
            });
            
            stream("notifications");
        }
    }
    <!-- Basic transition (default fade and scale) -->
    <div wire:transition>
        <p>This content will fade and scale in/out</p>
    </div>
    
    <!-- Opacity-only transition with custom duration -->
    <div wire:transition.opacity.duration.300ms>
        <p>This content will fade with 300ms duration</p>
    </div>
    
    <!-- Scale transition from top origin -->
    <div wire:transition.scale.origin.top.duration.250ms>
        <p>This content will scale from the top</p>
    </div>
    
    <!-- Out-only transition -->
    <div wire:transition.out.duration.500ms>
        <p>This content will only animate when disappearing</p>
    </div>
    // wires/ModernComponent.bx
    class extends="cbwire.models.Component" {
        function onMount() {
            // Component initialization logic
            loadInitialData();
        }
        
        function onUpdate() {
            // Called after component updates
            logUpdate();
        }
    }
    // wires/ModernComponent.cfc
    component extends="cbwire.models.Component" {
        function onMount() {
            // Component initialization logic
            loadInitialData();
        }
        
        function onUpdate() {
            // Called after component updates
            logUpdate();
        }
    }
    // Old (3.x)
    function mount() { }
    function updated() { }
    
    // New (4.x)
    function onMount() { }
    function onUpdate() { }
    // Remove these from config/ColdBox.bx
    moduleSettings = {
        "cbwire": {
            // Remove: "enableTurbo": true,
            // Remove: "throwOnMissingSetter": false,
        }
    };
    // Remove these from config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            // Remove: "enableTurbo" = true,
            // Remove: "throwOnMissingSetter" = false,
        }
    };
    // wires/UserManager.cfc
    component extends="cbwire.models.Component" {
        // Inject dependencies via properties
        property name="userService" inject="UserService";
        property name="logger" inject="logbox:logger:{this}";
        
        data = {
            "users" = [],
            "newUser" = {"name" = "", "email" = ""}
        };
    
        function onMount() {
            data.users = userService.getAll();
            logger.info("UserManager component mounted");
        }
    
        function addUser() {
            if (len(data.newUser.name) && len(data.newUser.email)) {
                userService.create(data.newUser);
                data.users = userService.getAll(); // Refresh list
                data.newUser = {"name" = "", "email" = ""}; // Reset form
                logger.info("User created: #data.newUser.name#");
            }
        }
    
        function deleteUser(userId) {
            userService.delete(userId);
            data.users = userService.getAll(); // Refresh list
            logger.info("User deleted: #userId#");
        }
    }
    <!-- wires/userManager.bxm -->
    <bx:output>
    <div>
        <h1>User Management</h1>
        
        <!-- Add new user form -->
        <form wire:submit="addUser">
            <input type="text" wire:model="newUser.name" placeholder="Name" required>
            <input type="email" wire:model="newUser.email" placeholder="Email" required>
            <button type="submit">Add User</button>
        </form>
        
        <!-- User list -->
        <div class="user-list">
            <bx:loop array="#users#" index="user">
                <div wire:key="user-#user.id#" class="user-item">
                    <span>#user.name# (#user.email#)</span>
                    <button wire:click="deleteUser(#user.id#)" wire:confirm="Delete this user?">
                        Delete
                    </button>
                </div>
            </bx:loop>
        </div>
    </div>
    </bx:output>
    <!-- wires/userManager.cfm -->
    <cfoutput>
    <div>
        <h1>User Management</h1>
        
        <!-- Add new user form -->
        <form wire:submit="addUser">
            <input type="text" wire:model="newUser.name" placeholder="Name" required>
            <input type="email" wire:model="newUser.email" placeholder="Email" required>
            <button type="submit">Add User</button>
        </form>
        
        <!-- User list -->
        <div class="user-list">
            <cfloop array="#users#" index="user">
                <div wire:key="user-#user.id#" class="user-item">
                    <span>#user.name# (#user.email#)</span>
                    <button wire:click="deleteUser(#user.id#)" wire:confirm="Delete this user?">
                        Delete
                    </button>
                </div>
            </cfloop>
        </div>
    </div>
    </cfoutput>
    // wires/ReportGenerator.bx
    class extends="cbwire.models.Component" {
        data = {
            "reportType": "",
            "reports": []
        };
    
        function generateReport() {
            // Dynamically get the appropriate service based on report type
            var serviceName = data.reportType & "ReportService";
            var reportService = getInstance(serviceName);
            
            data.reports = reportService.generate();
        }
    
        function exportData(format) {
            // Get exporter based on format
            var exporter = getInstance("exporters.#format#Exporter");
            return exporter.export(data.reports);
        }
    }
    // wires/ReportGenerator.cfc
    component extends="cbwire.models.Component" {
        data = {
            "reportType" = "",
            "reports" = []
        };
    
        function generateReport() {
            // Dynamically get the appropriate service based on report type
            var serviceName = data.reportType & "ReportService";
            var reportService = getInstance(serviceName);
            
            data.reports = reportService.generate();
        }
    
        function exportData(format) {
            // Get exporter based on format
            var exporter = getInstance("exporters.#format#Exporter");
            return exporter.export(data.reports);
        }
    }
    property name="userService" inject="UserService";
    property name="emailService" inject="EmailService";
    property name="cacheService" inject="CacheService";
    property name="logger" inject="logbox:logger:{this}";
    property name="settings" inject="coldbox:setting:myCustomSettings";
    property name="appSettings" inject="coldbox:fwSetting:applicationName";
    property name="apiClient" inject="provider:APIClientFactory";
    property name="configService" inject="id:ConfigurationService";
    /**
     * Get an instance object from WireBox
     *
     * @name The mapping name or CFC path or DSL to retrieve
     * @initArguments The constructor structure of arguments to passthrough when initializing the instance
     * @dsl The DSL string to use to retrieve an instance
     *
     * @return The requested instance
     */
    function getInstance(name, initArguments={}, dsl)
    // Basic retrieval
    var userService = getInstance("UserService");
    
    // With initialization arguments
    var apiClient = getInstance("APIClient", {
        "baseURL": "https://api.example.com",
        "timeout": 30
    });
    
    // Using DSL
    var logger = getInstance(dsl="logbox:logger:MyComponent");
    // WireBox lifecycle methods available in CBWIRE components
    function onDIComplete() {
        // Called after all dependencies are injected
    }
    
    function postInit() {
        // Called after object initialization
    }
    
    function preDestroy() {
        // Called before object destruction
    }
    <!-- layouts/Main.bxm -->
    <bx:output>
    <nav>
        <a href="/" wire:navigate>Dashboard</a>
        <a href="/blog" wire:navigate.hover>Blog</a>
        <a href="/users" wire:navigate>Users</a>
    </nav>
    
    <!-- Persisted audio player -->
    <div x-persist="player">
        <audio src="#episode.file#" controls></audio>
    </div>
    
    <main>
        #wire("BlogPost")#
    </main>
    </bx:output>
    <!-- layouts/Main.cfm -->
    <cfoutput>
    <nav>
        <a href="/" wire:navigate>Dashboard</a>
        <a href="/blog" wire:navigate.hover>Blog</a>
        <a href="/users" wire:navigate>Users</a>
    </nav>
    
    <!-- Persisted audio player -->
    <div x-persist="player">
        <audio src="#episode.file#" controls></audio>
    </div>
    
    <main>
        #wire("BlogPost")#
    </main>
    </cfoutput>
    function submit() {
        redirect("/thank-you", true); // Second parameter enables wire:navigate
    }
    function submit() {
        redirect("/thank-you", true); // Second parameter enables wire:navigate
    }
    <!-- layouts/Main.bxm -->
    <bx:output>
    <div x-persist="player">
        <audio src="#episode.file#" controls></audio>
    </div>
    
    <div x-persist="chat">
        <div class="chat-widget">Live chat...</div>
    </div>
    </bx:output>
    <!-- layouts/Main.cfm -->
    <cfoutput>
    <div x-persist="player">
        <audio src="#episode.file#" controls></audio>
    </div>
    
    <div x-persist="chat">
        <div class="chat-widget">Live chat...</div>
    </div>
    </cfoutput>
    <div x-persist="scrollbar">
        <div class="overflow-y-scroll" wire:scroll>
            <!-- Long scrollable content -->
        </div>
    </div>
    document.addEventListener('livewire:navigate', (event) => {
        // Triggered when navigation starts
        // Can prevent navigation: event.preventDefault()
        
        let context = event.detail;
        context.url;     // URL object of destination
        context.history; // True if back/forward navigation
        context.cached;  // True if cached version exists
    });
    
    document.addEventListener('livewire:navigating', () => {
        // Triggered before HTML swap
        // Good place to mutate HTML before navigation
    });
    
    document.addEventListener('livewire:navigated', () => {
        // Triggered after navigation completes
        // Use instead of DOMContentLoaded
    });
    Livewire.navigate('/new/url');
    // Run once per navigation
    document.addEventListener('livewire:navigated', () => {
        // Page-specific code
    }, { once: true });
    
    // Persistent across navigations
    document.addEventListener('livewire:navigated', () => {
        // Global code
    });
    <script src="https://cdn.usefathom.com/script.js" 
            data-site="ABCDEFG" 
            data-spa="auto" 
            defer></script>
    <head>
        <!-- Cache-busted scripts with tracking -->
        <script src="/app.js?id=123" data-navigate-track></script>
    </head>
    
    <body>
        <!-- Run only once -->
        <script data-navigate-once>
            console.log('Runs only on first evaluation');
        </script>
    </body>
    // config/ColdBox.bx
    moduleSettings = {
        "cbwire": {
            "showProgressBar": true,
            "progressBarColor": "##2299dd"
        }
    };
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "showProgressBar": true,
            "progressBarColor": "##2299dd"
        }
    };
    <script>
    document.addEventListener('livewire:navigate', (event) => {
        if (!window.navigating) {
            window.navigating = true;
            event.preventDefault();
            
            let target = event.detail;
            
            // Show custom loader
            let loader = document.createElement('div');
            loader.className = 'spinner-border text-primary';
            loader.setAttribute('role', 'status');
            loader.innerHTML = '<span class="visually-hidden">Loading...</span>';
            document.getElementById('content').appendChild(loader);
            
            Livewire.navigate(target.url.pathname);
        }
    }, { once: true });
    
    document.addEventListener('livewire:navigated', () => {
        window.navigating = false;
    });
    </script>
    // wires/FileUploader.cfc
    component extends="cbwire.models.Component" {
        data = {
            "uploadedFile" = "",
            "uploadedFiles" = [],
            "uploadProgress" = 0
        };
        
        function processUpload() {
            if (structKeyExists(data, "uploadedFile") && len(data.uploadedFile)) {
                // Handle single file upload
                var uploadPath = expandPath("./uploads/");
                fileMove(data.uploadedFile, uploadPath & data.uploadedFile.getClientFileName());
                
                data.uploadProgress = 100;
            }
        }
        
        function processMultipleUploads() {
            if (arrayLen(data.uploadedFiles)) {
                var uploadPath = expandPath("./uploads/");
                
                for (var file in data.uploadedFiles) {
                    fileMove(file, uploadPath & file.getClientFileName());
                }
                
                data.uploadProgress = 100;
            }
        }
    }
    <!-- wires/fileUploader.cfm -->
    <cfoutput>
    <div class="upload-container">
        <form wire:submit="processUpload">
            <label for="file">Choose File:</label>
            <input type="file" wire:model="uploadedFile" id="file">
            
            <cfif uploadProgress GT 0>
                <div class="progress">
                    <div class="progress-bar" style="width: #uploadProgress#%">#uploadProgress#%</div>
                </div>
            </cfif>
            
            <button type="submit">Upload File</button>
        </form>
        
        <form wire:submit="processMultipleUploads">
            <label for="files">Choose Multiple Files:</label>
            <input type="file" wire:model="uploadedFiles" id="files" multiple>
            <button type="submit">Upload Files</button>
        </form>
    </div>
    </cfoutput>
    // tests/specs/integration/ComponentTest.cfc
    component extends="cbwire.testing.BaseWireSpec" {
        
        function testCounterComponent() {
            var comp = visitComponent("Counter")
                .assertSee("0")
                .call("increment")
                .assertSee("1")
                .call("increment")
                .assertSee("2")
                .call("reset")
                .assertSee("0");
        }
        
        function testFormSubmission() {
            var comp = visitComponent("ContactForm")
                .set("name", "John Doe")
                .set("email", "[email protected]")
                .set("message", "Test message")
                .call("submitForm")
                .assertSee("Thank you")
                .assertDontSee("Please fill");
        }
        
        function testDataPersistence() {
            var comp = visitComponent("UserProfile", { "userId": 123 })
                .assertSee("John Doe")
                .set("firstName", "Jane")
                .call("updateProfile")
                .assertSee("Jane Doe")
                .assertDontSee("John Doe");
        }
    }
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "wiresDirectory" = "/custom/components/path"
        }
    };
    // Alternative: Set custom location per component
    component extends="cbwire.models.Component" {
        this.template = "/custom/templates/MyComponent.cfm";
    }
    // wires/APIHandler.cfc
    component extends="cbwire.models.Component" {
        data = {
            "status" = "idle",
            "result" = {}
        };
        
        function processAPICall() {
            data.status = "processing";
            
            try {
                data.result = callExternalAPI();
                data.status = "success";
            } catch (any e) {
                data.status = "error";
                data.result = { "error" = e.message };
            }
            
            // Skip template rendering for AJAX-only updates
            noRender();
        }
    }
    // wires/FormTracker.cfc
    component extends="cbwire.models.Component" {
        data = {
            "formData" = {},
            "hasChanges" = false,
            "changedFields" = []
        };
        
        function onUpdated() {
            // Track which properties are dirty
            var dirtyProps = getDirtyProperties();
            
            if (arrayLen(dirtyProps)) {
                data.hasChanges = true;
                data.changedFields = dirtyProps;
            }
        }
        
        function saveChanges() {
            if (data.hasChanges) {
                // Only save changed fields
                for (var field in data.changedFields) {
                    updateFieldInDatabase(field, data.formData[field]);
                }
                
                data.hasChanges = false;
                data.changedFields = [];
            }
        }
    }
    // wires/Dashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "users" = [],
            "orders" = []
        };
        
        // This expensive calculation runs only once per render
        function getTotalRevenue() {
            var total = 0;
            
            for (var order in data.orders) {
                total += order.amount;
            }
            
            return dollarFormat(total);
        }
        
        // This also runs once and caches the result
        function getUserStats() {
            return {
                "total" = arrayLen(data.users),
                "active" = data.users.filter(function(user) {
                    return user.status == "active";
                }).len(),
                "premium" = data.users.filter(function(user) {
                    return user.isPremium;
                }).len()
            };
        }
    }
    // wires/UserManager.cfc
    component extends="cbwire.models.Component" {
        
        property name="userService" inject="UserService";
        property name="emailService" inject="EmailService";
        property name="settings" inject="coldbox:moduleSettings:cbwire";
        
        data = {
            "users" = [],
            "selectedUser" = {}
        };
        
        function mount() {
            data.users = userService.getAllUsers();
        }
        
        function sendWelcomeEmail(userId) {
            var user = userService.getUser(userId);
            emailService.sendWelcomeEmail(user.email, user.firstName);
        }
    }
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "enableTurbo" = true,
            "moduleRootURI" = "/custom/cbwire/path"
        }
    };
    // wires/CustomComponent.cfc
    component extends="cbwire.models.Component" {
        this.template = "/custom/views/special-template.cfm";
        
        data = {
            "content" = "Custom template content"
        };
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        data = {
            "avatar" = null,  // Null is now a valid value
            "bio" = null,
            "website" = null
        };
        
        function clearAvatar() {
            data.avatar = null;  // Explicitly set to null
        }
    }

    What's New With 4.1

    09/29/2024

    New Features

    Limited BoxLang Support

    CBWIRE 4.1 introduces BoxLang support, allowing you to build components and templates using BoxLang's powerful, modular, and modern feature set. This opens up a whole new world of possibilities for CBWIRE development with BoxLang's enhanced syntax and capabilities.

    Requirements:

    • bx-compat-cfml BoxLang module

    • bx-esapi BoxLang module

    BoxLang brings modern language features like enhanced member functions, improved syntax, and better type handling to your CBWIRE components.

    Assets Management

    CBWIRE 4.1 introduces <cbwire:script> and <cbwire:assets> functionality for better control over component-specific assets. These assets are automatically tracked throughout the request and appended to the <head> tag during page load.

    Locked Data Properties

    Protect sensitive data properties from client-side modifications by defining a locked variable in your component. This prevents certain properties from being updated via wire:model or other client interactions.

    Module Root URL Configuration

    Configure custom module root URLs for better control over CBWIRE routing in complex applications.

    File Upload Enhancement

    New getTemporaryStoragePath() method in FileUpload components provides access to temporary file storage locations.

    Progress Bar Customization

    Enhanced progress bar configuration with proper color setting and disable functionality.

    String Trimming

    Automatically trim whitespace from string values in form inputs.

    External Module Support

    Load CBWIRE components from external module locations for better code organization.

    Update Endpoint Configuration

    Customize the CBWIRE update endpoint for advanced routing scenarios.

    Enhancements

    Asset Rendering Optimization

    • Assets are now tracked throughout the request and only rendered once

    • Component-specific assets are automatically injected into the <head> tag

    • Prevents duplicate asset loading for better performance

    Performance Improvements

    • CBWIRE rendering speed improvements

    • Enhanced child/nested component tracking

    • Optimized asset management and injection

    Bug Fixes

    Component Lifecycle

    • Fixed onRender() method: Restored missing onRender() method from previous versions

    • Fixed onUpdate() behavior: Resolved issue where onUpdate() was called when no updates occurred

    Component Management

    • Child/nested component tracking: Improved tracking of child components in Livewire

    • Component isolation: Better handling of component isolation and lazy loading

    Asset Management

    • Duplicate asset prevention: Assets that have already been rendered are not returned again

    • Asset injection: Component assets are properly tracked and injected into page head

    External Module Support

    • Module loading: Fixed issues loading wires from external module locations

    • Path resolution: Improved component path resolution for complex module structures

    Troubleshooting

    CBWIRE integrates complex technologies like Livewire's DOM diffing, Alpine.js reactivity, and server-side rendering. This guide addresses the most common issues you'll encounter and provides practical solutions with detailed examples.

    CBWIRE Component Issues

    Lazy-Loaded Components with Placeholders Don't Render

    Problem: You get a "Snapshot missing on Livewire component" error when using lazy loading with placeholders.

    Root Cause: Mismatch between placeholder and template outer elements confuses Livewire's DOM diffing engine.

    Error: Snapshot missing on Livewire component with id...

    Solution: Match outer elements exactly between placeholder and template.

    Conditional Rendering Issues

    Problem: Livewire fails to detect conditional sections appearing/disappearing, causing rendering glitches.

    Root Cause: Complex nested conditionals can confuse Livewire's DOM diffing algorithm.

    Solution: Add wire:key to conditional elements (recommended approach).

    Alternative Solution: If wire:key doesn't solve the issue, add conditional block markers.

    Block Marker Syntax:

    Server Configuration Issues

    400 Bad Request on CBWIRE Updates

    Problem: The /cbwire/update endpoint returns "400 Bad Request" errors, preventing CBWIRE components from updating.

    Root Cause: Server software or connectors stripping out the X-Livewire header that CBWIRE requires for request handling.

    Error: HTTP 400 status code when CBWIRE attempts to process component updates.

    Solution: Configure your server software to allow empty headers or preserve the X-Livewire header.

    BonCode AJP Connector (IIS + Lucee)

    If you're running Lucee behind IIS using the BonCode AJP Connector, you need to allow empty headers:

    File: /BIN/BonCodeAJP13.settings

    After making this change, restart your web server for the setting to take effect.

    Other Server Software

    The X-Livewire header is essential for CBWIRE to function properly. If you're experiencing 400 errors with other server configurations:

    1. Check server logs - Look for messages about rejected or stripped headers

    2. Review proxy settings - Ensure reverse proxies (nginx, Apache) pass through the X-Livewire header

    3. Check security modules - WAF or security modules might be filtering custom headers

    Example nginx configuration to preserve headers:

    Example Apache configuration to preserve headers:

    CBWIRE depends on the X-Livewire header for proper request routing and component identification. If this header is stripped or blocked by your server configuration, CBWIRE will return 400 errors and components will fail to update.

    Alpine.js Integration Issues

    Quote Syntax Errors in x-data

    Problem: JavaScript errors when using double quotes inside x-data attributes.

    Root Cause: HTML attribute already uses double quotes, creating syntax conflicts.

    Error: JavaScript syntax error in browser console.

    Solution: Use single quotes inside x-data blocks.

    Component Removal with Alpine x-if

    Problem: "Unable to find component" errors when using x-if with CBWIRE components.

    Root Cause: Alpine's x-if completely removes/recreates DOM elements, breaking Livewire's component tracking.

    Error: Unable to find component K8SDFLSDF902KSDFLASKJFASDFLJ.

    Solution: Replace x-if with x-show to preserve DOM elements.

    Key Differences: x-if vs x-show

    Directive
    Behavior
    Use with CBWIRE

    Quick Reference

    Essential Rules

    1. Placeholder-Template Matching: Outer elements must be identical

    2. Conditional Blocks: Wrap complex conditionals with <!--[if BLOCK]><![endif]--> markers

    3. Alpine Quotes: Always use single quotes inside x-data attributes

    Common Error Messages

    Error
    Likely Cause
    Solution

    When in doubt, add wire:key to dynamic elements and wrap conditionals with block markers. These techniques help Livewire's DOM diffing engine track changes accurately.

    JavaScript

    CBWIRE includes Alpine.js, and uses Livewire.js for all client-side functionality and DOM diffing. These tools give you complete control over client-side functionality while providing ways to interact with your component server-side.

    Alpine.js

    Alpine.js is included with CBWIRE and was designed to work beautifully with your CBWIRE components.

    Using Alpine.js is as easy as declaring an x-data in your . You can declare client-side properties, actions, and interact with your component using the $wire object.

    Nesting Components

    You can also pass parameters.

    // wires/UserDashboard.bx
    class extends="cbwire.models.Component" {
        data = {
            "users": [],
            "searchTerm": "",
            "selectedRole": "all"
        };
        
        function onMount() {
            data.users = getUserService().getAllUsers();
        }
        
        function filterUsers() {
            var filteredUsers = data.users
                .filter(function(user) {
                    return data.selectedRole == "all" || user.role == data.selectedRole;
                })
                .filter(function(user) {
                    return !data.searchTerm.len() || 
                           user.name.findNoCase(data.searchTerm) || 
                           user.email.findNoCase(data.searchTerm);
                });
            
            return filteredUsers;
        }
    }
    Test header presence
    - Use browser developer tools to verify the
    X-Livewire
    header is being sent
    Component Visibility: Use x-show instead of x-if with CBWIRE components

    x-if

    Removes/recreates DOM elements

    ❌ Breaks component tracking

    x-show

    Toggles CSS display property

    ✅ Preserves component tracking

    "Snapshot missing"

    Placeholder/template mismatch

    Match outer elements exactly

    "Unable to find component"

    Alpine x-if removing components

    Use x-show instead

    JavaScript syntax error

    Double quotes in x-data

    Use single quotes

    400 Bad Request

    X-Livewire header stripped by server

    Configure server to allow empty headers

    // wires/Widget.bx
    class extends="cbwire.models.Component" {
        function placeholder() {
            return "<div>put spinner here...</div>"; // ✅ Matches template
        }
    }
    // wires/Widget.cfc
    component extends="cbwire.models.Component" {
        function placeholder() {
            return "<div>put spinner here...</div>"; // ✅ Matches template
        }
    }
    <!-- wires/widget.bxm -->
    <bx:output>
    <div> <!-- ✅ Matches placeholder's <div> -->
        <h1>My Widget</h1>
    </div>
    </bx:output>
    <!-- wires/widget.cfm -->
    <cfoutput>
    <div> <!-- ✅ Matches placeholder's <div> -->
        <h1>My Widget</h1>
    </div>
    </cfoutput>
    Using Keys with Nested Components

    When nesting components, it's important to provide a key to help Livewire understand what's changed when components are updated. This is particularly crucial when you have dynamic nested components that may be added or removed from the DOM.

    Why Keys Matter

    Livewire treats each nested component as an island, meaning that each child component is truly its own separate UI component with its own data properties and functionality. Refreshing the parent does not automatically refresh the child components. However, sometimes you want to remove or delete child components entirely. The key parameter is what Livewire looks at to determine if a child component still exists or if it should be removed from the DOM.

    Without keys, when a parent component refreshes, Livewire may not properly identify which nested components have changed, leading to unexpected behavior or components not being properly cleaned up.

    Basic Key Usage

    Dynamic Keys for Component Lists

    When rendering multiple nested components dynamically, use unique keys to help Livewire track each component individually:

    Key Guidelines

    • Use unique identifiers: Base keys on database IDs, UUIDs, or other unique values

    • Keep keys stable: Don't use random values that change on each render unless you are wanting the child component to load new on every parent refresh

    • Be descriptive: Use meaningful key names like "user-#userID#" or "form-#formType#"

    • Required for dynamic lists: Always use keys when rendering nested components in loops

    • Essential for conditional rendering: Use keys when components are conditionally displayed

    When a key changes or is removed, Livewire will destroy the old component and create a new one. This ensures proper cleanup of event listeners, timers, and other component state.

    Keys must be unique across all nested components on the page, not just within a single parent component. Duplicate keys can cause rendering issues and unpredictable behavior.

    // wires/ContactUs.bx
    class extends="cbwire.models.Component" {
        data = {
            "showForm": false
        };  
    }
    // wires/ContactUs.cfc
    component extends="cbwire.models.Component" {
        data = {
            "showForm": false
        };  
    }
    <!--- wires/contactus.bxm --->
    <bx:output>
        <div>
            <bx:if showForm>
                <!--- Nested component --->
                #wire( name="ContactForm" )#
            </bx:if>    
            <button wire:click="$toggle( 'showForm' )">Toggle form</button>
        </div>
    </bx:output>
    <!--- wires/contactus.cfm --->
    <cfoutput>
        <div>
            <cfif showForm>
                <!--- Nested component --->
                #wire( name="ContactForm" )#
            </cfif>    
            <button wire:click="$toggle( 'showForm' )">Toggle form</button>
        </div>
    </cfoutput>
    // wires/ContactUs.bx
    class extends="cbwire.models.Component" {
        data = {
            "showForm": false,
            "sendEmail": false,
            "validateForm": true
        };  
        
        function onMount( params ) {
            data.sendEmail = params.sendEmail;
            data.validateForm = params.validateForm;
        }
    }
    // wires/ContactUs.cfc
    component extends="cbwire.models.Component" {
        data = {
            "showForm": false,
            "sendEmail": false,
            "validateForm": true
        };  
        
        function onMount( params ) {
            data.sendEmail = params.sendEmail;
            data.validateForm = params.validateForm;
        }
    }

    Learn more on the Alpine.js features page.

    $wire object

    The $wire object is one of the most powerful features in Livewire.

    With $wire, you get a JavaScript representation of your server-side CBWIRE component that you can interact with. You can change data properties, call actions, access parent components, fire events, and much more.

    Below are the properties and methods of the $wire object:

    Executing Scripts

    You can execute bespoke JavaScript in your CBWIRE component using <cbwire:script>.

    Here's a complete example:

    Scripts are executed after the page has loaded BUT before your CBWIRE component has rendered.

    Unlike assets, which are loaded only once per page, scripts are evaluated for every component instance.

    Loading Assets

    You can load style assets on the page with your component using <cbwire:assets>.

    Here's an example of loading a date picker called Pikaday:

    Livewire will ensure any assets are loaded on the page before evaluating scripts. It will also ensure that assets are only loaded once per page, even if you have multiple instances of the component.

    Using $wire From Scripts

    You can access your component's $wire object within your scripts block.

    Livewire Events

    You can access the livewire:init and livewire:initialized events as Livewire is loading.

    Livewire object

    You can interact with Livewire from external scripts using the Livewire global object.

    Custom Directives

    Livewire allows you to register custom directives using Livewire.directive().

    Here's the implementation for wire:confirm:

    JavaScript Hooks

    Intercepting Commits

    A commit is made every time a CBWIRE component is sent to the server. You can hook into an individual commit like this:

    Request Hooks

    Using the request hook, you can hook into the entire HTTP request, which may contain multiple commits.

    Customizing Page Expiration

    If the default page expiration dialog isn't suitable for your application, you can use the request hook to implement a custom solution.

    template
    // wires/UserDashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "users" = [],
            "searchTerm" = "",
            "selectedRole" = "all"
        };
        
        function onMount() {
            data.users = getUserService().getAllUsers();
        }
        
        function filterUsers() {
            var filteredUsers = data.users
                .filter(function(user) {
                    return data.selectedRole == "all" || user.role == data.selectedRole;
                })
                .filter(function(user) {
                    return !len(data.searchTerm) || 
                           findNoCase(data.searchTerm, user.name) || 
                           findNoCase(data.searchTerm, user.email);
                });
            
            return filteredUsers;
        }
    }
    <!-- wires/userDashboard.bxm -->
    <bx:output>
    <div class="user-dashboard">
        <div class="filters">
            <input wire:model.live="searchTerm" placeholder="Search users...">
            <select wire:model.live="selectedRole">
                <option value="all">All Roles</option>
                <option value="admin">Admin</option>
                <option value="user">User</option>
            </select>
        </div>
        
        <div class="users-grid">
            <bx:loop array="#filterUsers()#" item="user">
                <div class="user-card">
                    <h3>#user.name#</h3>
                    <p>#user.email#</p>
                    <span class="role">#user.role#</span>
                </div>
            </bx:loop>
        </div>
    </div>
    </bx:output>
    <!-- wires/userDashboard.cfm -->
    <cfoutput>
    <div class="user-dashboard">
        <div class="filters">
            <input wire:model.live="searchTerm" placeholder="Search users...">
            <select wire:model.live="selectedRole">
                <option value="all">All Roles</option>
                <option value="admin">Admin</option>
                <option value="user">User</option>
            </select>
        </div>
        
        <div class="users-grid">
            <cfloop array="#filterUsers()#" index="user">
                <div class="user-card">
                    <h3>#user.name#</h3>
                    <p>#user.email#</p>
                    <span class="role">#user.role#</span>
                </div>
            </cfloop>
        </div>
    </div>
    </cfoutput>
    <!-- wires/dashboard.bxm -->
    <bx:output>
    <cbwire:script>
        console.log('Dashboard component loaded');
        // Component-specific JavaScript
    </cbwire:script>
    
    <cbwire:assets>
        <link rel="stylesheet" href="/css/dashboard.css">
        <script src="/js/chart-library.js"></script>
    </cbwire:assets>
    
    <div>
        <h1>Dashboard</h1>
        <!-- Component content -->
    </div>
    </bx:output>
    <!-- wires/dashboard.cfm -->
    <cfoutput>
    <cbwire:script>
        console.log('Dashboard component loaded');
        // Component-specific JavaScript
    </cbwire:script>
    
    <cbwire:assets>
        <link rel="stylesheet" href="/css/dashboard.css">
        <script src="/js/chart-library.js"></script>
    </cbwire:assets>
    
    <div>
        <h1>Dashboard</h1>
        <!-- Component content -->
    </div>
    </cfoutput>
    // wires/UserProfile.bx
    class extends="cbwire.models.Component" {
        // Lock single property
        locked = "userId";
        
        // Or lock multiple properties
        locked = ["userId", "role", "permissions"];
        
        data = {
            "userId": 123,
            "name": "John Doe",
            "email": "[email protected]",
            "role": "admin",
            "permissions": ["read", "write"]
        };
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        // Lock single property
        locked = "userId";
        
        // Or lock multiple properties
        locked = ["userId", "role", "permissions"];
        
        data = {
            "userId" = 123,
            "name" = "John Doe",
            "email" = "[email protected]",
            "role" = "admin",
            "permissions" = ["read", "write"]
        };
    }
    <!-- Template can bind to name/email but userId/role are protected -->
    <input wire:model="name" placeholder="Name">
    <input wire:model="email" placeholder="Email">
    <!-- These would throw "Locked properties cannot be updated" if attempted -->
    // config/ColdBox.bx
    moduleSettings = {
        "cbwire": {
            "moduleRootURL": "/custom/cbwire/path"
        }
    };
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "moduleRootURL" = "/custom/cbwire/path"
        }
    };
    // wires/FileManager.bx
    class extends="cbwire.models.Component" {
        function processUpload() {
            if (data.upload) {
                var tempPath = data.upload.getTemporaryStoragePath();
                // Process file at temporary location
            }
        }
    }
    // wires/FileManager.cfc
    component extends="cbwire.models.Component" {
        function processUpload() {
            if (structKeyExists(data, "upload")) {
                var tempPath = data.upload.getTemporaryStoragePath();
                // Process file at temporary location
            }
        }
    }
    // config/ColdBox.bx
    moduleSettings = {
        "cbwire": {
            "showProgressBar": false,           // Disable completely
            "progressBarColor": "##00ff00"      // Custom color (fixed)
        }
    };
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "showProgressBar" = false,          // Disable completely
            "progressBarColor" = "##00ff00"     // Custom color (fixed)
        }
    };
    // Global setting
    moduleSettings = {
        "cbwire": {
            "trimStringValues": true
        }
    };
    
    // Or per component
    class extends="cbwire.models.Component" {
        function shouldTrimStringValues() {
            return true;
        }
    }
    // Global setting
    moduleSettings = {
        "cbwire" = {
            "trimStringValues" = true
        }
    };
    
    // Or per component
    component extends="cbwire.models.Component" {
        function shouldTrimStringValues() {
            return true;
        }
    }
    // config/ColdBox.bx
    moduleSettings = {
        "cbwire": {
            "updateEndpoint": "/custom/cbwire/update"
        }
    };
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "updateEndpoint" = "/custom/cbwire/update"
        }
    };
    <!-- wires/userForm.bxm -->
    <bx:output>
    <div>
        <h1>User Registration</h1>
        
        <!-- ✅ Using wire:key (recommended) -->
        <bx:if errorList.len()>
            <div class="alert alert-danger" role="alert" wire:key="error-alert">
                <p>Please fix the following errors:</p>
                <ul>
                    <bx:loop array="#errorList#" index="error">
                        <li>#error#</li>
                    </bx:loop>
                </ul>
            </div>
        </bx:if>
        
        <form wire:submit="submit">
            <input type="text" wire:model="name" placeholder="Name">
            <input type="email" wire:model="email" placeholder="Email">
            <button type="submit">Register</button>
        </form>
    </div>
    </bx:output>
    <!-- wires/userForm.cfm -->
    <cfoutput>
    <div>
        <h1>User Registration</h1>
        
        <!-- ✅ Using wire:key (recommended) -->
        <cfif arrayLen(errorList)>
            <div class="alert alert-danger" role="alert" wire:key="error-alert">
                <p>Please fix the following errors:</p>
                <ul>
                    <cfloop array="#errorList#" index="error">
                        <li>#error#</li>
                    </cfloop>
                </ul>
            </div>
        </cfif>
        
        <form wire:submit="submit">
            <input type="text" wire:model="name" placeholder="Name">
            <input type="email" wire:model="email" placeholder="Email">
            <button type="submit">Register</button>
        </form>
    </div>
    </cfoutput>
    <!-- wires/userForm.bxm -->
    <bx:output>
    <div>
        <h1>User Registration</h1>
        
        <!-- ✅ Wrapped conditional with block markers -->
        <!--[if BLOCK]><![endif]-->
        <bx:if errorList.len()>
            <div class="alert alert-danger" role="alert">
                <p>Please fix the following errors:</p>
                <ul>
                    <bx:loop array="#errorList#" index="error">
                        <li>#error#</li>
                    </bx:loop>
                </ul>
            </div>
        </bx:if>
        <!--[if ENDBLOCK]><![endif]-->
        
        <form wire:submit="submit">
            <input type="text" wire:model="name" placeholder="Name">
            <input type="email" wire:model="email" placeholder="Email">
            <button type="submit">Register</button>
        </form>
    </div>
    </bx:output>
    <!-- wires/userForm.cfm -->
    <cfoutput>
    <div>
        <h1>User Registration</h1>
        
        <!-- ✅ Wrapped conditional with block markers -->
        <!--[if BLOCK]><![endif]-->
        <cfif arrayLen(errorList)>
            <div class="alert alert-danger" role="alert">
                <p>Please fix the following errors:</p>
                <ul>
                    <cfloop array="#errorList#" index="error">
                        <li>#error#</li>
                    </cfloop>
                </ul>
            </div>
        </cfif>
        <!--[if ENDBLOCK]><![endif]-->
        
        <form wire:submit="submit">
            <input type="text" wire:model="name" placeholder="Name">
            <input type="email" wire:model="email" placeholder="Email">
            <button type="submit">Register</button>
        </form>
    </div>
    </cfoutput>
    <!--[if BLOCK]><![endif]-->
    <!-- Your conditional content here -->
    <!--[if ENDBLOCK]><![endif]-->
    <!-- Change from False to True -->
    <AllowEmptyHeaders>True</AllowEmptyHeaders>
    location /cbwire/ {
        proxy_pass http://your-backend;
        proxy_set_header X-Livewire $http_x_livewire;
        proxy_pass_request_headers on;
    }
    <Location /cbwire/>
        ProxyPreserveHost On
        ProxyPass http://your-backend/cbwire/
        ProxyPassReverse http://your-backend/cbwire/
    </Location>
    <!-- ✅ Correct syntax -->
    <div x-data="{
        name: 'CBWIRE',           // ✅ Single quotes work properly
        message: 'Welcome user',  // ✅ Single quotes work properly
        count: 0,
        increment() { this.count++ }
    }">
        <div>Name: <span x-text="name"></span></div>
        <div>Message: <span x-text="message"></span></div>
        <div>Count: <span x-text="count"></span></div>
        <button @click="increment">Increment</button>
    </div>
    <!-- wires/dashboard.bxm -->
    <bx:output>
    <div x-data="{
        loading: false,
        async init() {
            this.loading = true;
            await $wire.loadData();
            this.loading = false;
        }
    }">
        <h1>Dashboard</h1>
        
        <!-- ✅ x-show preserves DOM elements -->
        <div x-show="loading">
            #wire("Spinner")#  <!-- ✅ Component stays tracked -->
        </div>
        
        <div x-show="!loading">
            <p>Dashboard content loaded!</p>
        </div>
    </div>
    </bx:output>
    <!-- wires/dashboard.cfm -->
    <cfoutput>
    <div x-data="{
        loading: false,
        async init() {
            this.loading = true;
            await $wire.loadData();
            this.loading = false;
        }
    }">
        <h1>Dashboard</h1>
        
        <!-- ✅ x-show preserves DOM elements -->
        <div x-show="loading">
            #wire("Spinner")#  <!-- ✅ Component stays tracked -->
        </div>
        
        <div x-show="!loading">
            <p>Dashboard content loaded!</p>
        </div>
    </div>
    </cfoutput>
    <!--- wires/contactus.bxm --->
    <bx:output>
        <div>
            <bx:if showForm>
                <!--- Using a key helps Livewire track this nested component --->
                #wire( name="ContactForm", key="contact-form-#generateUUID()#" )#
            </bx:if>    
            <button wire:click="$toggle( 'showForm' )">Toggle form</button>
        </div>
    </bx:output>
    <!--- wires/contactus.cfm --->
    <cfoutput>
        <div>
            <cfif showForm>
                <!--- Using a key helps Livewire track this nested component --->
                #wire( name="ContactForm", key="contact-form-#createUUID()#" )#
            </cfif>    
            <button wire:click="$toggle( 'showForm' )">Toggle form</button>
        </div>
    </cfoutput>
    // wires/PostList.bx
    class extends="cbwire.models.Component" {
        data = {
            "posts": [
                { "id": 1, "title": "First Post" },
                { "id": 2, "title": "Second Post" },
                { "id": 3, "title": "Third Post" }
            ]
        };
    
        function removePost(postId) {
            data.posts = data.posts.filter(function(post) {
                return post.id != arguments.postId;
            });
        }
    }
    // wires/PostList.cfc
    component extends="cbwire.models.Component" {
        data = {
            "posts" = [
                { "id" = 1, "title" = "First Post" },
                { "id" = 2, "title" = "Second Post" },
                { "id" = 3, "title" = "Third Post" }
            ]
        };
    
        function removePost(postId) {
            data.posts = data.posts.filter(function(post) {
                return post.id != arguments.postId;
            });
        }
    }
    <!--- wires/postlist.bxm --->
    <bx:output>
        <div>
            <bx:loop array="#posts#" index="post">
                <!--- Each nested component gets a unique key based on the post ID --->
                #wire( 
                    name="PostCard", 
                    params={ "post": post },
                    key="post-#post.id#"
                )#
            </bx:loop>
        </div>
    </bx:output>
    <!--- wires/postlist.cfm --->
    <cfoutput>
        <div>
            <cfloop array="#posts#" index="post">
                <!--- Each nested component gets a unique key based on the post ID --->
                #wire( 
                    name="PostCard", 
                    params={ "post": post },
                    key="post-#post.id#"
                )#
            </cfloop>
        </div>
    </cfoutput>
    <!--- wires/contactus.bxm --->
    <bx:output>
        <div>
            <bx:if showForm>
                #wire(
                    name="ContactForm"
                    params={
                        sendEmail: true,
                        validateForm: true
                    }
                )#
            </bx:if>
            <button wire:click="$toggle( 'showForm' )">Toggle form</button>    
        </div>
    </bx:output>
    <!--- wires/contactus.cfm --->
    <cfoutput>
        <div>
            <cfif showForm>
                #wire(
                    name="ContactForm"
                    params={
                        sendEmail: true,
                        validateForm: true
                    }
                )#
            </cfif>
            <button wire:click="$toggle( 'showForm' )">Toggle form</button>    
        </div>
    </cfoutput>
    <div
        x-data="{
            counter: 0,
            increment() {
                this.counter++   
            },
            async save() {
                // Call the save method on our component
                await $wire.save( this.counter );
            }
        }"
        wire:ignore.self>
        <div>Count: <span x-text="counter"></span></div>
        <button @click="increment">+</button>
        <button @click="save">Save to database</button>
    </div>
    let $wire = {
        // All component public properties are directly accessible on $wire...
        counter: 0,
     
        // All public methods are exposed and callable on $wire...
        increment() { ... },
     
        // Access the `$wire` object of the parent component if one exists...
        $parent,
     
        // Access the root DOM element of the CBWIRE component...
        $el,
     
        // Access the ID of the current CBWIRE component...
        $id,
     
        // Get the value of a property by name...
        // Usage: $wire.$get('count')
        $get(name) { ... },
     
        // Set a property on the component by name...
        // Usage: $wire.$set('count', 5)
        $set(name, value, live = true) { ... },
     
        // Toggle the value of a boolean property...
        $toggle(name, live = true) { ... },
     
        // Call the method
        // Usage: $wire.$call('increment')
        $call(method, ...params) { ... },
     
        // Entangle the value of a CBWIRE property with a different,
        // arbitrary, Alpine property...
        // Usage: <div x-data="{ count: $wire.$entangle('count') }">
        $entangle(name, live = false) { ... },
     
        // Watch the value of a property for changes...
        // Usage: Alpine.$watch('counter', (value, old) => { ... })
        $watch(name, callback) { ... },
     
        // Refresh a component by sending a commit to the server
        // to re-render the HTML and swap it into the page...
        $refresh() { ... },
     
        // Identical to the above `$refresh`. Just a more technical name...
        $commit() { ... },
     
        // Listen for a an event dispatched from this component or its children...
        // Usage: $wire.$on('post-created', () => { ... })
        $on(event, callback) { ... },
     
        // Dispatch an event from this component...
        // Usage: $wire.$dispatch('post-created', { postId: 2 })
        $dispatch(event, params = {}) { ... },
     
        // Dispatch an event onto another component...
        // Usage: $wire.$dispatchTo('dashboard', 'post-created', { postId: 2 })
        $dispatchTo(otherComponentName, event, params = {}) { ... },
     
        // Dispatch an event onto this component and no others...
        $dispatchSelf(event, params = {}) { ... },
     
        // A JS API to upload a file directly to component
        // rather than through `wire:model`...
        $upload(
            name, // The property name
            file, // The File JavaScript object
            finish = () => { ... }, // Runs when the upload is finished...
            error = () => { ... }, // Runs if an error is triggered mid-upload...
            progress = (event) => { // Runs as the upload progresses...
                event.detail.progress // An integer from 1-100...
            },
        ) { ... },
     
        // API to upload multiple files at the same time...
        $uploadMultiple(name, files, finish, error, progress) { },
     
        // Remove an upload after it's been temporarily uploaded but not saved...
        $removeUpload(name, tmpFilename, finish, error) { ... },
     
        // Retrieve the underlying "component" object...
        __instance() { ... },
    }
    <!--- ./wires/mycomponent.bxm|cfm --->
    <div>...</div>
    
    <cbwire:script>
        <script>
            // This Javascript will get executed every time this component is loaded onto the page...
        </script>
    </cbwire:script>
    <!--- ./wires/counter.bxm|cfm --->
    <div>
        Counter component in Alpine:
     
        <div x-data="counter">
            <h1 x-text="count"></h1>
            <button x-on:click="increment">+</button>
        </div>
    </div>
     
    <cbwire:script>
        <script>
            Alpine.data('counter', () => {
                return {
                    count: 0,
                    increment() {
                        this.count++
                    },
                }
            })
        </script>
    </cbwire:script>
    <!--- ./wires/date.bxm|cfm --->
    <div>
        <input type="text" data-picker>
    </div>
     
    <cbwire:assets>
        <script src="https://cdn.jsdelivr.net/npm/pikaday/pikaday.js" defer></script>
        <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/pikaday/css/pikaday.css">
    </cbwire:assets>
     
    <cbwire:script>
        <script>
            new Pikaday({ field: $wire.$el.querySelector('[data-picker]') });
        </script>
    </cbwire:script>
    <cbwire:script>
        <script>
            setInterval( () => {
                $wire.$refresh()
            }, 3000 );
        </script>
    </cbwire:script>
    <script>
        document.addEventListener('livewire:init', () => {
            // Runs after Livewire is loaded but before it's initialized
            // on the page...
        } );
     
        document.addEventListener('livewire:initialized', () => {
            // Runs immediately after Livewire has finished initializing
            // on the page...
        } );
    </script>
    // Retrieve the $wire object for the first component on the page...
    let component = Livewire.first()
     
    // Retrieve a given component's `$wire` object by its ID...
    let component = Livewire.find(id);
     
    // Retrieve an array of component `$wire` objects by name...
    let components = Livewire.getByName(name);
     
    // Retrieve $wire objects for every component on the page...
    let components = Livewire.all();
    
    // Dispatch an event to any CBWIRE components listening...
    Livewire.dispatch( 'post-created', { postId: 2 } );
     
    // Dispatch an event to a given CBWIRE component by name...
    Livewire.dispatchTo( 'dashboard', 'post-created', { postId: 2 } );
     
    // Listen for events dispatched from CBWIRE components...
    Livewire.on('post-created', ( { postId } ) => {
        // ...
    } );
    
    // Register a callback to execute on a given internal Livewire hook...
    Livewire.hook( 'component.init', ( { component, cleanup } ) => {
        // ...
    })
    <button wire:confirm="Are you sure?" wire:click="delete">Delete post</button>
    Livewire.directive( 'confirm', ( { el, directive, component, cleanup } ) => {
        let content =  directive.expression;
     
        // The "directive" object gives you access to the parsed directive.
        // For example, here are its values for: wire:click.prevent="deletePost(1)"
        //
        // directive.raw = wire:click.prevent;
        // directive.value = "click";
        // directive.modifiers = ['prevent'];
        // directive.expression = "deletePost(1)";
     
        let onClick = e => {
            if (! confirm( content ) ) {
                e.preventDefault()
                e.stopImmediatePropagation()
            }
        }
     
        el.addEventListener( 'click', onClick, { capture: true } );
     
        // Register any cleanup code inside `cleanup()` in the case
        // where a Livewire component is removed from the DOM while
        // the page is still active.
        cleanup( () => {
            el.removeEventListener( 'click', onClick );
        } );
    })
    Livewire.hook('component.init', ({ component, cleanup }) => {
        // Fires every time a new component is discovered by Livewire
        // Both on page load and later on
    })
    
    Livewire.hook('element.init', ({ component, el }) => {
        // Fires for each DOM element within a given CBWIRE component
    })
    
    Livewire.hook('morph.updating',  ({ el, component, toEl, skip, childrenOnly }) => {
        // DOM morphing hook
    })
     
    Livewire.hook('morph.updated', ({ el, component }) => {
        // DOM morphing hook
    })
     
    Livewire.hook('morph.removing', ({ el, component, skip }) => {
        // DOM morphing hook
    })
     
    Livewire.hook('morph.removed', ({ el, component }) => {
        // DOM morphing hook
    })
     
    Livewire.hook('morph.adding',  ({ el, component }) => {
        // DOM morphing hook
    })
     
    Livewire.hook('morph.added',  ({ el }) => {
        // DOM morphing hook
    })
    Livewire.hook('commit.prepare', ({ component, commit }) => {
        // Runs before commit payloads are collected and sent to the server...
    })
    
    Livewire.hook('commit', ({ component, commit, respond, succeed, fail }) => {
        // Runs immediately before a commit's payload is sent to the server...
     
        respond(() => {
            // Runs after a response is received but before it's processed...
        })
     
        succeed(({ snapshot, effect }) => {
            // Runs after a successful response is received and processed
            // with a new snapshot and list of effects...
        })
     
        fail(() => {
            // Runs if some part of the request failed...
        })
    })
    Livewire.hook('request', ({ uri, options, payload, respond, succeed, fail }) => {
        // Runs after commit payloads are compiled, but before a network request is sent...
     
        respond(({ status, response }) => {
            // Runs when the response is received...
            // "response" is the raw HTTP response object
            // before await response.text() is run...
        })
     
        succeed(({ status, json }) => {
            // Runs when the response is received...
            // "json" is the JSON response object...
        })
     
        fail(({ status, content, preventDefault }) => {
            // Runs when the response has an error status code...
            // "preventDefault" allows you to disable Livewire's
            // default error handling...
            // "content" is the raw response content...
        })
    })
    <script>
        document.addEventListener('livewire:init', () => {
            Livewire.hook('request', ({ fail }) => {
                fail(({ status, preventDefault }) => {
                    if (status === 419) {
                        confirm('Your custom page expiration behavior...')
                        preventDefault()
                    }
                })
            })
        })
    </script>

    What's New With 3.1

    09/25/2023

    New Features

    Auto-Include CBWIRE Assets

    CBWIRE 3.1 introduces automatic inclusion of styles and scripts, eliminating the need to manually call wireScripts() and wireStyles() in your templates.

    Previously required manual inclusion:

    Now assets are automatically included when components are rendered.

    Enhanced Lifecycle Hooks

    New onUpdate() and onUpdateProperty() lifecycle methods provide granular control over component updates.

    Child Component Refresh

    Parent components can now refresh all child components using the new refresh functionality.

    UDF Access in Templates

    Components can now call CBWIRE User Defined Functions directly from templates, expanding beyond computed properties.

    Single File Components Improvements

    Enhanced single file component handling with better caching and collision prevention.

    Single file components now automatically clear on fwreinit and have improved file name collision handling for high-traffic applications.

    Enhanced onMount() Parameters

    Support for params as an alias for parameters in onMount() method calls.

    Template Helper Access

    Templates can now access ColdBox application helpers and module helpers directly.

    Reset Methods Enhancement

    New resetExcept() method allows resetting all data properties except specified ones.

    Enhancements

    Performance Improvements

    • Faster rendering with optimized view caching and lookups

    • Improved single file component compilation and caching

    • Better memory management for high-traffic applications

    Template Variable Cleanup

    • Removed unnecessary template variables to prevent collisions

    • Cleaner template scope with better variable isolation

    • Improved debugging experience

    Child Component Management

    • Fixed child components not re-rendering on follow-up requests

    • Better tracking and lifecycle management for nested components

    • Improved parent-child communication

    Bug Fixes

    Template Rendering

    • Fixed HTML comments breaking re-renders: HTML comments in templates no longer interfere with component re-rendering

    • Fixed child component rendering: Child components now properly re-render on subsequent requests

    Single File Components

    • Fixed file name collisions: Improved handling of SFC file names in high-traffic applications

    • Automatic cleanup: Compiled SFCs are now properly cleared on fwreinit

    Breaking Changes

    This release maintains backward compatibility with existing CBWIRE 3.x applications. The automatic asset inclusion is enabled by default but can be disabled if needed.

    Security

    CBWIRE provides multiple ways to secure your wire components, from simple authentication checks to integration with the cbSecurity module for role and permission-based authorization.

    onSecure() Lifecycle Method

    The onSecure() lifecycle method runs before all other lifecycle methods, allowing you to enforce security rules at the earliest point in the request lifecycle. Return false to halt processing and render an empty div, or return nothing to continue processing normally.

    Data Properties

    Data Properties hold the state of your component and are defined with a data structure. Each data property is assigned a default value and is automatically synchronized between server and client.

    Data properties are reactive - when an action modifies a property, CBWIRE automatically re-renders only the affected parts of your template. They can hold strings, numbers, booleans, dates, arrays, and structures, and are directly accessible in templates without special syntax.

    JavaScript parses your data properties. Your data properties can only store JavaScript-friendly values: strings, numerics, arrays, structs, or booleans.

    Form Validation

    Validate forms using ColdBox's engine. CBWIRE automatically validates components on each request when constraints are defined, and provides helper methods for displaying validation errors.

    Installation

    Install cbValidation via CommandBox:

    Single or double quotes must surround property names.

    JavaScript is a case-sensitive language, and CFML isn't. To preserve the casing of your property names, you must surround them with quotes.

    Don't do this.

    Data properties are visible to JavaScript. You SHOULD NOT store sensitive data in them. Use locked properties for additional protection against client-side modifications.

    Accessing From Actions

    Using the data structure, you can access data properties from within your component actions.

    Accessing From Templates

    You can access data properties within your templates using #propertyName#.

    Nested Properties

    Organize related data using nested structures and access them with dot notation. This keeps your data organized hierarchically instead of flattening everything into top-level properties.

    Accessing in Templates

    Reference nested properties using dot notation in wire:model and other directives:

    Accessing in Actions

    Use dot notation to access and modify nested properties in your component methods:

    Lifecycle Hooks

    Lifecycle hooks for nested properties use underscores instead of dots. A property path like user.name.first triggers onUpdateuser_name_first():

    Missing nested keys are created automatically. If data.user doesn't have a preferences key, setting data.user.preferences.theme = "dark" creates the structure automatically.

    Resetting Properties

    You can reset all data properties to their original default value inside your actions using reset().

    You can reset individual, some, or all data properties.

    You can also reset all properties EXCEPT the properties you pass.

    Locked Properties

    Locked properties prevent client-side modifications to sensitive data like user IDs, roles, or permissions. Define a locked variable to protect specific properties from wire:model and other client interactions. CBWIRE throws an exception if locked properties are modified from the client.

    Single Property

    Multiple Properties

    Template Usage

    Locked properties can be displayed but not modified through form inputs:

    Common Use Cases

    • User Management: Lock userId, role, permissions

    • E-commerce: Lock product IDs, prices, inventory counts

    • Financial: Lock account numbers, balances, transaction IDs

    • Content: Lock author IDs, creation dates, publication status

    Locked properties only prevent client-side modifications. Server-side code in your action methods can still modify locked properties when necessary.

    Remember that locked properties are still visible in the client-side JavaScript. For truly sensitive data that shouldn't be visible to users, avoid including it in data properties altogether and access it through server-side services instead.

    //./wires/SomeComponent.bx
    class extends="cbwire.models.Component" {
        data = {
            "propertyName": "defaultValue"
        };
    }
    //./wires/SomeComponent.cfc
    component extends="cbwire.models.Component" {
        data = {
            "propertyName": "defaultValue"
        };
    }
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "autoIncludeAssets" = true  // Default: true
        }
    };
    <!-- Old way - no longer needed -->
    <head>
        #wireStyles()#
    </head>
    <body>
        <!-- content -->
        #wireScripts()#
    </body>
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        data = {
            "name" = "John Doe",
            "email" = "[email protected]",
            "lastUpdated" = now()
        };
        
        function onUpdate() {
            // Called after any component update
            data.lastUpdated = now();
            logActivity("Profile updated");
        }
        
        function onUpdateProperty(propertyName, newValue, oldValue) {
            // Called when specific properties change
            if (propertyName == "email") {
                sendEmailVerification(newValue);
            }
        }
        
        function updateProfile() {
            // This will trigger onUpdate() and onUpdateProperty()
            data.name = "Jane Doe";
            data.email = "[email protected]";
        }
    }
    // wires/Dashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "stats" = {},
            "notifications" = []
        };
        
        function refreshAllData() {
            // Refresh parent data
            data.stats = getLatestStats();
            data.notifications = getNotifications();
            
            // Refresh all child components
            refresh();
        }
    }
    <!-- wires/dashboard.cfm -->
    <cfoutput>
    <div class="dashboard">
        <button wire:click="refreshAllData">Refresh All</button>
        
        <div class="stats">
            #wire("StatsComponent")#
        </div>
        
        <div class="notifications">
            #wire("NotificationComponent")#
        </div>
    </div>
    </cfoutput>
    // wires/ProductCatalog.cfc
    component extends="cbwire.models.Component" {
        data = {
            "products" = [],
            "category" = "all"
        };
        
        function onMount() {
            data.products = getProducts();
        }
        
        function formatPrice(price) {
            return dollarFormat(price);
        }
        
        function isOnSale(product) {
            return product.salePrice < product.regularPrice;
        }
        
        function getCategoryName(categoryId) {
            return getCategory(categoryId).name;
        }
    }
    <!-- wires/productCatalog.cfm -->
    <cfoutput>
    <div class="product-catalog">
        <h2>Category: #getCategoryName(category)#</h2>
        
        <cfloop array="#products#" index="product">
            <div class="product #isOnSale(product) ? 'on-sale' : ''#">
                <h3>#product.name#</h3>
                <p class="price">
                    <cfif isOnSale(product)>
                        <span class="regular">#formatPrice(product.regularPrice)#</span>
                        <span class="sale">#formatPrice(product.salePrice)#</span>
                    <cfelse>
                        #formatPrice(product.regularPrice)#
                    </cfif>
                </p>
            </div>
        </cfloop>
    </div>
    </cfoutput>
    // config/ColdBox.cfc
    moduleSettings = {
        "cbwire" = {
            "cacheSingleFileComponents" = true  // Cache compiled SFCs
        }
    };
    // wires/ReportViewer.cfc
    component extends="cbwire.models.Component" {
        data = {
            "report" = {},
            "reportId" = ""
        };
        
        // Both 'parameters' and 'params' work
        function onMount(params = {}) {
            if (structKeyExists(params, "reportId")) {
                data.reportId = params.reportId;
                data.report = getReport(params.reportId);
            }
        }
    }
    <!-- applicationHelper.cfm -->
    <cfscript>
    function formatDate(date) {
        return dateFormat(date, "mm/dd/yyyy");
    }
    
    function truncateText(text, length = 50) {
        return left(text, length) & (len(text) > length ? "..." : "");
    }
    
    function formatCurrency(amount) {
        return dollarFormat(amount);
    }
    </cfscript>
    <!-- wires/blogPost.cfm -->
    <cfoutput>
    <article class="blog-post">
        <h2>#title#</h2>
        <p class="meta">Published: #formatDate(publishedDate)#</p>
        <p class="excerpt">#truncateText(content, 150)#</p>
    </article>
    </cfoutput>
    // wires/UserForm.cfc
    component extends="cbwire.models.Component" {
        data = {
            "name" = "",
            "email" = "",
            "phone" = "",
            "formSubmitted" = false,
            "errors" = {}
        };
        
        function clearForm() {
            // Reset everything except submission status
            resetExcept("formSubmitted");
        }
        
        function clearFormKeepEmail() {
            // Reset everything except email and submission status
            resetExcept(["email", "formSubmitted"]);
        }
    }
    data = {
        propertyName: "defaultValue"
    };
    <bx:output>
        <div>
            <h1>Current time</h1>
            <div>#time#</div>
            <button wire:click="updateTime">Update</button>
        </div>
    </bx:output>
    <cfoutput>
        <div>
            <h1>Current time</h1>
            <div>#time#</div>
            <button wire:click="updateTime">Update</button>
        </div>
    </cfoutput>
    // wires/UserProfile.bx
    class extends="cbwire.models.Component" {
        data = {
            "user": {
                "name": {
                    "first": "John",
                    "last": "Doe"
                },
                "contact": {
                    "email": "[email protected]",
                    "phone": "555-1234"
                },
                "preferences": {
                    "theme": "dark",
                    "notifications": true
                }
            }
        };
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        data = {
            "user" = {
                "name" = {
                    "first" = "John",
                    "last" = "Doe"
                },
                "contact" = {
                    "email" = "[email protected]",
                    "phone" = "555-1234"
                },
                "preferences" = {
                    "theme" = "dark",
                    "notifications" = true
                }
            }
        };
    }
    <!-- wires/userProfile.bxm -->
    <bx:output>
    <form wire:submit="saveProfile">
        <input type="text" wire:model="user.name.first" placeholder="First Name">
        <input type="text" wire:model="user.name.last" placeholder="Last Name">
        <input type="email" wire:model="user.contact.email" placeholder="Email">
        <input type="tel" wire:model="user.contact.phone" placeholder="Phone">
    
        <select wire:model="user.preferences.theme">
            <option value="light">Light</option>
            <option value="dark">Dark</option>
        </select>
    
        <label>
            <input type="checkbox" wire:model="user.preferences.notifications">
            Enable notifications
        </label>
    
        <button type="submit">Save Profile</button>
    </form>
    </bx:output>
    <!-- wires/userProfile.cfm -->
    <cfoutput>
    <form wire:submit="saveProfile">
        <input type="text" wire:model="user.name.first" placeholder="First Name">
        <input type="text" wire:model="user.name.last" placeholder="Last Name">
        <input type="email" wire:model="user.contact.email" placeholder="Email">
        <input type="tel" wire:model="user.contact.phone" placeholder="Phone">
    
        <select wire:model="user.preferences.theme">
            <option value="light">Light</option>
            <option value="dark">Dark</option>
        </select>
    
        <label>
            <input type="checkbox" wire:model="user.preferences.notifications">
            Enable notifications
        </label>
    
        <button type="submit">Save Profile</button>
    </form>
    </cfoutput>
    // wires/UserProfile.bx
    class extends="cbwire.models.Component" {
        data = {
            "user": {
                "name": {
                    "first": "John",
                    "last": "Doe"
                },
                "contact": {
                    "email": "[email protected]"
                }
            }
        };
    
        function saveProfile() {
            // Access nested properties
            var fullName = data.user.name.first & " " & data.user.name.last;
    
            // Modify nested properties
            data.user.contact.email = data.user.contact.email.trim();
    
            // Save to database
            userService.update( data.user );
        }
    
        function updateTheme( theme ) {
            data.user.preferences.theme = theme;
        }
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        data = {
            "user" = {
                "name" = {
                    "first" = "John",
                    "last" = "Doe"
                },
                "contact" = {
                    "email" = "[email protected]"
                }
            }
        };
    
        function saveProfile() {
            // Access nested properties
            var fullName = data.user.name.first & " " & data.user.name.last;
    
            // Modify nested properties
            data.user.contact.email = trim( data.user.contact.email );
    
            // Save to database
            userService.update( data.user );
        }
    
        function updateTheme( theme ) {
            data.user.preferences.theme = theme;
        }
    }
    // wires/UserProfile.bx
    class extends="cbwire.models.Component" {
        data = {
            "user": {
                "contact": {
                    "email": "[email protected]"
                }
            }
        };
    
        function onUpdateuser_contact_email( value, oldValue ) {
            // Triggered when user.contact.email changes
            writeLog( "Email changed from #oldValue# to #value#" );
    
            // Validate email format
            if ( !isValid( "email", value ) ) {
                data.user.contact.email = oldValue;
                addError( "user.contact.email", "Please enter a valid email address" );
            }
        }
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        data = {
            "user" = {
                "contact" = {
                    "email" = "[email protected]"
                }
            }
        };
    
        function onUpdateuser_contact_email( value, oldValue ) {
            // Triggered when user.contact.email changes
            writeLog( "Email changed from #oldValue# to #value#" );
    
            // Validate email format
            if ( !isValid( "email", value ) ) {
                data.user.contact.email = oldValue;
                addError( "user.contact.email", "Please enter a valid email address" );
            }
        }
    }
    // wires/UserProfile.bx
    class extends="cbwire.models.Component" {
        // Lock single property
        locked = "userId";
        
        data = {
            "userId": 123,
            "name": "John Doe",
            "email": "[email protected]",
            "isActive": true
        };
        
        function updateProfile() {
            // Can modify name, email, isActive
            // Cannot modify userId - it's locked
        }
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        // Lock single property
        locked = "userId";
        
        data = {
            "userId" = 123,
            "name" = "John Doe",
            "email" = "[email protected]",
            "isActive" = true
        };
        
        function updateProfile() {
            // Can modify name, email, isActive
            // Cannot modify userId - it's locked
        }
    }
    // wires/AdminPanel.bx
    class extends="cbwire.models.Component" {
        // Lock multiple properties
        locked = ["userId", "role", "permissions", "accountType"];
        
        data = {
            "userId": 123,
            "name": "Jane Admin",
            "email": "[email protected]",
            "role": "administrator",
            "permissions": ["read", "write", "delete", "admin"],
            "accountType": "premium",
            "lastLogin": now(),
            "theme": "dark"
        };
        
        function updateSettings() {
            // Can modify: name, email, lastLogin, theme
            // Cannot modify: userId, role, permissions, accountType
            data.lastLogin = now();
            data.theme = "light";
        }
    }
    // wires/AdminPanel.cfc
    component extends="cbwire.models.Component" {
        // Lock multiple properties
        locked = ["userId", "role", "permissions", "accountType"];
        
        data = {
            "userId" = 123,
            "name" = "Jane Admin",
            "email" = "[email protected]",
            "role" = "administrator",
            "permissions" = ["read", "write", "delete", "admin"],
            "accountType" = "premium",
            "lastLogin" = now(),
            "theme" = "dark"
        };
        
        function updateSettings() {
            // Can modify: name, email, lastLogin, theme
            // Cannot modify: userId, role, permissions, accountType
            data.lastLogin = now();
            data.theme = "light";
        }
    }
    <!-- wires/adminPanel.bxm -->
    <bx:output>
    <div>
        <h1>User Profile</h1>
        
        <!-- Display locked properties (read-only) -->
        <div class="readonly-info">
            <p><strong>User ID:</strong> #userId#</p>
            <p><strong>Role:</strong> #role#</p>
            <p><strong>Account Type:</strong> #accountType#</p>
        </div>
        
        <!-- Form with editable properties -->
        <form wire:submit="updateSettings">
            <!-- These inputs work normally -->
            <input type="text" wire:model="name" placeholder="Name">
            <input type="email" wire:model="email" placeholder="Email">
            
            <select wire:model="theme">
                <option value="light">Light</option>
                <option value="dark">Dark</option>
            </select>
            
            <!-- These would throw an exception if attempted -->
            <!-- <input type="hidden" wire:model="userId"> ❌ -->
            <!-- <input type="text" wire:model="role"> ❌ -->
            
            <button type="submit">Update Profile</button>
        </form>
    </div>
    </bx:output>
    <!-- wires/adminPanel.cfm -->
    <cfoutput>
    <div>
        <h1>User Profile</h1>
        
        <!-- Display locked properties (read-only) -->
        <div class="readonly-info">
            <p><strong>User ID:</strong> #userId#</p>
            <p><strong>Role:</strong> #role#</p>
            <p><strong>Account Type:</strong> #accountType#</p>
        </div>
        
        <!-- Form with editable properties -->
        <form wire:submit="updateSettings">
            <!-- These inputs work normally -->
            <input type="text" wire:model="name" placeholder="Name">
            <input type="email" wire:model="email" placeholder="Email">
            
            <select wire:model="theme">
                <option value="light">Light</option>
                <option value="dark">Dark</option>
            </select>
            
            <!-- These would throw an exception if attempted -->
            <!-- <input type="hidden" wire:model="userId"> ❌ -->
            <!-- <input type="text" wire:model="role"> ❌ -->
            
            <button type="submit">Update Profile</button>
        </form>
    </div>
    </cfoutput>
    // Data properties
    data = {
        "time": now()
    }; 
       
    // Action
    function updateTime(){
        data.time = now();
    }
    data = {
        "time": now()
    };
    
    function resetTime(){
        reset();
    }
    function resetForm() {
        reset( "message" ); // resets 'message' data property
        reset( [ "message", "anotherprop" ] ); // reset multiple properties at once
        reset(); // resets all properties
    }
    function resetForm() {
        resetExcept( "email" );
        resetExcept( [ "email", "phoneNumber" ] );
    }

    Parameters

    Parameter
    Type
    Description

    event

    object

    The ColdBox request context

    rc

    struct

    Request collection

    prc

    struct

    Private request collection

    isInitial

    boolean

    True for initial mount, false for subsequent AJAX requests

    onSecure() fires on every request—both initial rendering and all subsequent AJAX requests. This ensures security checks run continuously throughout the component's lifecycle.

    Custom Failure Messages

    By default, returning false from onSecure() renders an empty div. Customize this message per component or globally:

    Set a global default in your ColdBox configuration:

    Component-level settings take precedence over module-level configuration.

    cbSecurity Integration

    When the cbSecurity module is installed and active, CBWIRE automatically integrates with it, enabling security annotations on components and methods.

    Component-Level Security

    Secure an entire component with the secured annotation:

    Method-Level Security

    Secure specific actions with annotations:

    Annotation Formats

    cbSecurity annotations support multiple formats:

    Security Interceptor

    CBWIRE fires the onCBWIRESecureFail interception point when security checks fail, allowing custom handling like logging or redirects.

    Register the interceptor in your ColdBox configuration:

    Checksum Validation

    CBWIRE automatically validates component data integrity using HMAC-SHA256 checksums to prevent tampering with data between client and server. Every component snapshot includes a checksum that validates the data hasn't been modified.

    How It Works

    When data travels between client and server, CBWIRE:

    1. Calculates a cryptographic checksum of the component snapshot using HMAC-SHA256

    2. Includes the checksum with the data payload

    3. Validates the checksum on subsequent requests

    4. Throws CBWIRECorruptPayloadException if validation fails

    The checksum uses a secret key—either a custom secret you provide or a hash of the module's root path.

    Configuration

    Control checksum validation in your ColdBox configuration:

    Disabling checksum validation removes protection against client-side data tampering. Only disable this in controlled development environments where you trust all clients.

    If you don't provide a custom secret, CBWIRE generates one automatically based on the module's installation path. For production deployments across multiple servers, set a consistent secret key.

    Error Handling

    Checksum validation failures throw CBWIRECorruptPayloadException in these scenarios:

    • Invalid JSON: Payload is not properly formatted JSON

    • Missing Checksum: No checksum found in the payload

    • Checksum Mismatch: Calculated checksum doesn't match the provided checksum

    These errors typically indicate:

    • Client-side data tampering attempts

    • Network corruption during transmission

    • Version mismatches between client and server code

    • Different secret keys across application servers

    CSRF Protection

    CBWIRE includes built-in Cross-Site Request Forgery (CSRF) protection to prevent malicious websites from submitting unauthorized requests to your application. CSRF tokens are automatically generated and validated for all component interactions.

    How It Works

    CSRF protection operates transparently:

    1. CBWIRE generates a unique token when components mount

    2. The token is included with every client-server interaction

    3. Server validates the token before processing requests

    4. Invalid or missing tokens reject the request

    No changes to your component code are required—CSRF protection works automatically when enabled.

    Configuration

    CSRF protection is enabled by default in CBWIRE 5.x. Configure it in your ColdBox settings:

    Storage Implementations

    CBWIRE provides two built-in storage implementations for CSRF tokens:

    SessionCSRFStorage (Default)

    Session-based storage is the OWASP-recommended approach and the default in CBWIRE 5.x:

    Benefits:

    • Eliminates "Page Expired" errors common with cache-based storage

    • Follows OWASP security recommendations

    • Simpler configuration for single-server deployments

    Requirements:

    • Application sessions must be enabled

    CacheCSRFStorage

    Cache-based storage is designed for distributed and clustered deployments:

    Benefits:

    • Works across multiple application servers

    • No session requirement

    • Suitable for stateless architectures

    Considerations:

    • Requires distributed cache configuration

    • Uses cookie/URL fallback when cache is unavailable

    Custom Storage

    Implement custom CSRF storage (Redis, DynamoDB, etc.) by creating a component that implements the ICSRFStorage interface:

    Configure your custom implementation:

    Changes in 5.x

    CBWIRE 5.x refactored CSRF protection with significant improvements:

    Session Storage by Default:

    • Changed from cache-based to session-based storage

    • Eliminates "Page Expired" errors

    • Aligns with OWASP recommendations

    Extensible Architecture:

    • Interface-based design allows custom implementations

    • Easy integration with any storage backend

    • Two built-in implementations for common scenarios

    Simplified Configuration:

    • Single csrfEnabled setting to enable/disable

    • Flexible csrfStorage setting for storage backend

    • No dependency on external CSRF modules

    Backward Compatibility:

    • Public API remains unchanged

    • Existing code continues working without modifications

    • Optional migration to session storage

    Security annotations are only active when cbSecurity is installed and configured. Without cbSecurity, use the onSecure() lifecycle method for custom security logic.

    Security checks run on every request. For performance-critical components, consider caching authorization results or using efficient permission lookups.

    // wires/AdminDashboard.bx
    class extends="cbwire.models.Component" {
        property name="authService" inject="AuthService";
    
        data = {
            "stats": {}
        };
    
        function onSecure( event, rc, prc, isInitial, params ) {
            // Check authentication
            if ( !authService.isLoggedIn() ) {
                return false;
            }
    
            // Check authorization
            if ( !authService.hasRole( "admin" ) ) {
                return false;
            }
        }
    
        function onMount() {
            data.stats = getAdminStats();
        }
    }
    Basic Usage

    Create a user registration form with validation:

    // wires/UserRegistration.bx
    class extends="cbwire.models.Component" {
        data = {
            "name": "",
            "email": "",
            "password": "",
            
    

    Validation Methods

    validateOrFail()

    Validates data and silently stops function execution if validation fails (does not throw an exception):

    validate()

    Returns a ValidationResult object for manual error handling:

    Error Display Methods

    All Errors

    Display all validation errors:

    Field-Specific Errors

    Display errors for specific fields:

    Common Constraints

    Most frequently used validation constraints:

    Constraint
    Description
    Example

    required

    Field must have a value

    {"required": true}

    type

    Field must be specific type

    {"type": "email"}

    size

    String length or numeric range

    {"size": "8..50"}

    min/max

    Minimum/maximum value

    Advanced Validation

    Custom Constraints

    Define validation constraints inline:

    Conditional Validation

    Use requiredIf and requiredUnless for conditional validation:

    CBWIRE automatically validates components when constraints are defined. Validation runs on each request before actions execute.

    For complete constraint documentation, see cbValidation documentation.

    cbValidation

    What's New With 5.0

    [Release Date TBD]

    Enhancements

    Livewire v3.6.4

    Upgraded the underlying Livewire JavaScript to v3.6.4, bringing new features including:

    • wire:current - Directive allows you to easily detect and style currently active links on a page

    • wire:cloak - Hides elements until Livewire initializes, preventing flash of unstyled content during page load

    • wire:show - Toggle element visibility using CSS without removing elements from the DOM, enabling smooth transitions

    BoxLang Support

    Full BoxLang support with enhanced performance and modern language features. Components use .bx for classes and .bxm for templates. All documentation includes BoxLang examples alongside CFML.

    Upload Error Handling

    The onUploadError() lifecycle hook provides graceful error handling for failed uploads. In 4.x, upload errors threw exceptions.

    The hook receives property (string), errors (any), and multiple (boolean) parameters. Triggered when uploads fail due to HTTP errors, network issues, or server unavailability.

    See the documentation for details.

    Event Object Access

    Access the ColdBox event object (request context) directly in templates to use methods like event.buildLink() for generating URLs.

    Background requests to /cbwire/update may not have access to RC/PRC values set by interceptors or handlers. Store needed values as data properties in onMount() to ensure they persist across re-renders.

    Dot Notation Data Properties

    Work with nested data structures using dot-separated paths in wire:model and component code. Reference hierarchical data naturally without flattening your structure.

    Missing keys are created automatically. Lifecycle hooks use underscores—user.name.first triggers onUpdateuser_name_first(newValue, oldValue).

    Single File Components

    Define both template and logic in a single .bxm file. Use <bx:output> for HTML and <bx:script> with // @startWire and // @endWire comments for component code.

    See the documentation for details.

    Empty Component Error Messages

    Clearer error messages for empty component templates. Contributed by David Moreno.

    Reduced DI Error Logging

    Eliminated unnecessary "ioc.Injector" error messages during single file component initialization.

    Error Handling Improvements

    Enhanced snapshot deserialization and effects processing. Invalid JSON now returns empty struct instead of throwing errors. Improved HTML entity encoding for content with quotes.

    Thread-Safe Component Building

    Added locking mechanisms for thread-safe single file component building, preventing race conditions.

    External Module Support

    Load wire components from external module locations via modulesExternalLocation configuration. Reference using @module syntax: wire( name="MyComponent@externalModule" ).

    CSRF Protection

    Built-in Cross-Site Request Forgery protection prevents unauthorized requests. CSRF tokens are automatically generated and validated for all component interactions, with no changes required to component code.

    Session Storage by Default: CBWIRE 5.x uses session-based storage (OWASP-recommended), eliminating "Page Expired" errors common with cache-based approaches.

    Flexible Storage Options: Choose between SessionCSRFStorage (default) for single-server deployments or CacheCSRFStorage for distributed systems. Implement custom storage backends using the ICSRFStorage interface.

    Configuration:

    See the documentation for complete details.

    Breaking Changes

    Secure Upload Storage

    File uploads now use the system temporary directory by default instead of the module's internal directory, preventing unauthorized access. Use the new store() method to move files to permanent storage.

    Migration: Replace fileWrite(uploadPath, data.photo.get()) with data.photo.store(path). The store() method is more efficient, creates directories automatically, and returns the stored file path.

    See the documentation for details.

    Parameter Auto-Population

    When a component defines onMount(), parameters passed via wire() are no longer automatically set to data properties. Explicitly assign parameters inside onMount().

    Migration: Add explicit assignments in onMount(): data.propertyName = params.propertyName.

    See the documentation for details.

    Engine Support Updates

    Added Lucee 6.0+ support. Removed Adobe ColdFusion 2018 and 2021 as both have reached end-of-life.

    Supported engines: BoxLang, Lucee 5.3+/6.0+, Adobe ColdFusion 2023+/2025+.

    ColdBox Support Updates

    Added ColdBox 7+ and 8+ support. Supported versions: ColdBox 6+, 7+, 8+.

    Bug Fixes

    Upload Endpoint Configuration

    File uploads now respect the updateEndpoint configuration setting instead of using hardcoded /cbwire/upload path.

    Upload Directory Path

    Fixed incorrect path generation in getUploadTempDirectory() method.

    Directory Race Condition

    Fixed race condition when multiple requests create temporary directory simultaneously. Added proper locking mechanisms.

    Lazy Loading Error

    Fixed lazy loading attempting to call non-existent onMount() methods. Now checks for method existence before calling.

    Wires Location Config

    Fixed wiresLocation configuration setting not working with custom values. Now properly applies custom locations.

    // wires/SecureContent.bx
    class extends="cbwire.models.Component" {
        property name="authService" inject="AuthService";
    
        // Custom message when security check fails
        secureMountFailMessage = "<div class='alert alert-warning'>Please log in to view this content.</div>";
    
        data = {
            "content": ""
        };
    
        function onSecure( event, rc, prc, isInitial, params ) {
            if ( !authService.isLoggedIn() ) {
                return false;
            }
        }
    }
    // wires/SecureContent.cfc
    component extends="cbwire.models.Component" {
        property name="authService" inject="AuthService";
    
        // Custom message when security check fails
        secureMountFailMessage = "<div class='alert alert-warning'>Please log in to view this content.</div>";
    
        data = {
            "content" = ""
        };
    
        function onSecure( event, rc, prc, isInitial, params ) {
            if ( !authService.isLoggedIn() ) {
                return false;
            }
        }
    }
    // wires/AdminPanel.bx
    @secured
    class extends="cbwire.models.Component" {
        data = {
            "users": []
        };
    
        function onMount() {
            data.users = userService.getAll();
        }
    }
    // wires/AdminPanel.cfc
    component extends="cbwire.models.Component" secured {
        data = {
            "users" = []
        };
    
        function onMount() {
            data.users = userService.getAll();
        }
    }
    // wires/UserManagement.bx
    class extends="cbwire.models.Component" {
        data = {
            "users": []
        };
    
        function onMount() {
            data.users = userService.getAll();
        }
    
        // Only users with "admin" permission can execute
        @secured("admin")
        function deleteUser( userId ) {
            userService.delete( userId );
            data.users = userService.getAll();
        }
    
        // Multiple permissions (user needs any one)
        @secured("admin,moderator")
        function updateUser( userId ) {
            userService.update( userId, data.users );
        }
    }
    // wires/UserManagement.cfc
    component extends="cbwire.models.Component" {
        data = {
            "users" = []
        };
    
        function onMount() {
            data.users = userService.getAll();
        }
    
        // Only users with "admin" permission can execute
        function deleteUser( userId ) secured="admin" {
            userService.delete( userId );
            data.users = userService.getAll();
        }
    
        // Multiple permissions (user needs any one)
        function updateUser( userId ) secured="admin,moderator" {
            userService.update( userId, data.users );
        }
    }
    // Boolean - uses cbSecurity default rules
    @secured
    function someAction() {}
    
    @secured(true)
    function someAction() {}
    
    // Single permission
    @secured("admin")
    function someAction() {}
    
    // Multiple permissions (comma-separated)
    @secured("admin,moderator,editor")
    function someAction() {}
    // Boolean - uses cbSecurity default rules
    function someAction() secured {}
    function someAction() secured=true {}
    
    // Single permission
    function someAction() secured="admin" {}
    
    // Multiple permissions (comma-separated)
    function someAction() secured="admin,moderator,editor" {}
    // models/RedisCSRFStorage.bx
    class implements="cbwire.models.ICSRFStorage" {
        property name="redisClient" inject="RedisClient";
    
        function set( key, value ) {
            redisClient.set( key, value );
        }
    
        function get( key ) {
            return redisClient.get( key );
        }
    
        function exists( key ) {
            return redisClient.exists( key );
        }
    
        function delete( key ) {
            redisClient.del( key );
        }
    
        function clear() {
            // Implement based on your requirements
        }
    }
    // models/RedisCSRFStorage.cfc
    component implements="cbwire.models.ICSRFStorage" {
        property name="redisClient" inject="RedisClient";
    
        function set( key, value ) {
            redisClient.set( key, value );
        }
    
        function get( key ) {
            return redisClient.get( key );
        }
    
        function exists( key ) {
            return redisClient.exists( key );
        }
    
        function delete( key ) {
            redisClient.del( key );
        }
    
        function clear() {
            // Implement based on your requirements
        }
    }
    // wires/AdminDashboard.cfc
    component extends="cbwire.models.Component" {
        property name="authService" inject="AuthService";
    
        data = {
            "stats" = {}
        };
    
        function onSecure( event, rc, prc, isInitial, params ) {
            // Check authentication
            if ( !authService.isLoggedIn() ) {
                return false;
            }
    
            // Check authorization
            if ( !authService.hasRole( "admin" ) ) {
                return false;
            }
        }
    
        function onMount() {
            data.stats = getAdminStats();
        }
    }
    // config/ColdBox.cfc
    moduleSettings = {
        cbwire = {
            secureMountFailMessage = "<div class='alert alert-danger'>Access Denied</div>"
        }
    };
    // interceptors/SecurityLogger.cfc
    component {
        function onCBWIRESecureFail( event, data, rc, prc ) {
            var componentName = data.componentName ?: "Unknown";
            var methodName = data.methodName ?: "N/A";
            var user = auth.user();
    
            writeLog(
                type = "warning",
                file = "security",
                text = "Security failure: User ##user.getId()## attempted to access #componentName#.#methodName#"
            );
    
            // Optional: redirect to login
            if ( !auth.check() ) {
                relocate( "login" );
            }
        }
    }
    // config/ColdBox.cfc
    interceptors = [
        { class="interceptors.SecurityLogger" }
    ];
    // config/ColdBox.cfc
    moduleSettings = {
        cbwire = {
            // Disable checksum validation (not recommended for production)
            checksumValidation = false,
    
            // Optional: provide a custom secret key
            secret = "your-secret-key-here"
        }
    };
    // config/ColdBox.cfc
    moduleSettings = {
        cbwire = {
            // Enable/disable CSRF protection (enabled by default)
            csrfEnabled = true,
    
            // Storage implementation (defaults to SessionCSRFStorage)
            csrfStorage = "SessionCSRFStorage@cbwire"
        }
    };
    moduleSettings = {
        cbwire = {
            csrfStorage = "SessionCSRFStorage@cbwire"
        }
    };
    moduleSettings = {
        cbwire = {
            csrfStorage = "CacheCSRFStorage@cbwire"
        }
    };
    moduleSettings = {
        cbwire = {
            csrfStorage = "RedisCSRFStorage@myapp"
        }
    };
    // wires/UserRegistration.cfc
    component extends="cbwire.models.Component" {
        data = {
            "name" = "",
            "email" = "",
            "password" = "",
            "confirmPassword" = ""
        };
        
        constraints = {
            "name" = {
                "required" = true,
                "requiredMessage" = "Name is required",
                "size" = "2..50"
            },
            "email" = {
                "required" = true,
                "requiredMessage" = "Email is required",
                "type" = "email"
            },
            "password" = {
                "required" = true,
                "requiredMessage" = "Password is required",
                "size" = "8..50"
            },
            "confirmPassword" = {
                "required" = true,
                "sameAs" = "password",
                "sameAsMessage" = "Passwords must match"
            }
        };
        
        function register() {
            validateOrFail();
            
            // Save user data
            // redirect("/dashboard");
        }
    }
    <!-- wires/userRegistration.bxm -->
    <bx:output>
    <div>
        <h1>User Registration</h1>
        
        <bx:if hasErrors()>
            <div class="alert alert-danger">
                <h4>Please fix the following errors:</h4>
                <ul>
                    <bx:loop array="#getErrors()#" item="error">
                        <li>#error#</li>
                    </bx:loop>
                </ul>
            </div>
        </bx:if>
        
        <form wire:submit.prevent="register">
            <div class="form-group">
                <label for="name">Name:</label>
                <input type="text" 
                       id="name" 
                       wire:model.blur="name" 
                       class="form-control #hasError('name') ? 'is-invalid' : ''#">
                <bx:if hasError("name")>
                    <span class="invalid-feedback">#getError("name")#</span>
                </bx:if>
            </div>
            
            <div class="form-group">
                <label for="email">Email:</label>
                <input type="email" 
                       id="email" 
                       wire:model.blur="email" 
                       class="form-control #hasError('email') ? 'is-invalid' : ''#">
                <bx:if hasError("email")>
                    <span class="invalid-feedback">#getError("email")#</span>
                </bx:if>
            </div>
            
            <div class="form-group">
                <label for="password">Password:</label>
                <input type="password" 
                       id="password" 
                       wire:model.blur="password" 
                       class="form-control #hasError('password') ? 'is-invalid' : ''#">
                <bx:if hasError("password")>
                    <span class="invalid-feedback">#getError("password")#</span>
                </bx:if>
            </div>
            
            <div class="form-group">
                <label for="confirmPassword">Confirm Password:</label>
                <input type="password" 
                       id="confirmPassword" 
                       wire:model.blur="confirmPassword" 
                       class="form-control #hasError('confirmPassword') ? 'is-invalid' : ''#">
                <bx:if hasError("confirmPassword")>
                    <span class="invalid-feedback">#getError("confirmPassword")#</span>
                </bx:if>
            </div>
            
            <button type="submit" class="btn btn-primary">Register</button>
        </form>
    </div>
    </bx:output>
    <!-- wires/userRegistration.cfm -->
    <cfoutput>
    <div>
        <h1>User Registration</h1>
        
        <cfif hasErrors()>
            <div class="alert alert-danger">
                <h4>Please fix the following errors:</h4>
                <ul>
                    <cfloop array="#getErrors()#" item="error">
                        <li>#error#</li>
                    </cfloop>
                </ul>
            </div>
        </cfif>
        
        <form wire:submit.prevent="register">
            <div class="form-group">
                <label for="name">Name:</label>
                <input type="text" 
                       id="name" 
                       wire:model.blur="name" 
                       class="form-control #hasError('name') ? 'is-invalid' : ''#">
                <cfif hasError("name")>
                    <span class="invalid-feedback">#getError("name")#</span>
                </cfif>
            </div>
            
            <div class="form-group">
                <label for="email">Email:</label>
                <input type="email" 
                       id="email" 
                       wire:model.blur="email" 
                       class="form-control #hasError('email') ? 'is-invalid' : ''#">
                <cfif hasError("email")>
                    <span class="invalid-feedback">#getError("email")#</span>
                </cfif>
            </div>
            
            <div class="form-group">
                <label for="password">Password:</label>
                <input type="password" 
                       id="password" 
                       wire:model.blur="password" 
                       class="form-control #hasError('password') ? 'is-invalid' : ''#">
                <cfif hasError("password")>
                    <span class="invalid-feedback">#getError("password")#</span>
                </cfif>
            </div>
            
            <div class="form-group">
                <label for="confirmPassword">Confirm Password:</label>
                <input type="password" 
                       id="confirmPassword" 
                       wire:model.blur="confirmPassword" 
                       class="form-control #hasError('confirmPassword') ? 'is-invalid' : ''#">
                <cfif hasError("confirmPassword")>
                    <span class="invalid-feedback">#getError("confirmPassword")#</span>
                </cfif>
            </div>
            
            <button type="submit" class="btn btn-primary">Register</button>
        </form>
    </div>
    </cfoutput>
    box install cbvalidation
    function register() {
        validateOrFail(); // Stops execution here if validation fails
        
        // This code only runs if validation passes
        // Save user data
        redirect("/dashboard");
    }
    function register() {
        var result = validate();
        
        if (!result.hasErrors()) {
            // Save user data
            redirect("/dashboard");
        } else {
            // Handle errors manually if needed
        }
    }
    hasErrors()    // Returns true if any validation errors exist
    getErrors()    // Returns array of all error messages
    hasError("fieldName")    // Returns true if field has errors
    getError("fieldName")    // Returns error message for field
    function processForm() {
        var result = validate(
            target = data,
            constraints = {
                "email": {"required": true, "type": "email"},
                "age": {"required": true, "min": 18}
            }
        );
        
        if (!result.hasErrors()) {
            // Process form
        }
    }
    constraints = {
        "phone": {
            "requiredIf": {"contactMethod": "phone"},
            "type": "telephone"
        },
        "email": {
            "requiredUnless": {"contactMethod": "phone"},
            "type": "email"
        }
    };

    params

    struct

    Parameters passed to the component via wire()

    "confirmPassword"
    :
    ""
    };
    constraints = {
    "name": {
    "required": true,
    "requiredMessage": "Name is required",
    "size": "2..50"
    },
    "email": {
    "required": true,
    "requiredMessage": "Email is required",
    "type": "email"
    },
    "password": {
    "required": true,
    "requiredMessage": "Password is required",
    "size": "8..50"
    },
    "confirmPassword": {
    "required": true,
    "sameAs": "password",
    "sameAsMessage": "Passwords must match"
    }
    };
    function register() {
    validateOrFail();
    // Save user data
    // redirect("/dashboard");
    }
    }

    {"min": 1, "max": 100}

    sameAs

    Must match another field

    {"sameAs": "password"}

    regex

    Must match regular expression

    {"regex": "^[A-Z].*"}

    inList

    Value must be in list

    {"inList": "red,blue,green"}

    wire:text - Dynamically update element text content without network roundtrips, perfect for optimistic UIs

  • wire:replace - Force elements to render from scratch instead of DOM diffing, useful for third-party libraries and web components

  • File Upload Error Handling
    Single-file Components
    Security
    File Uploads
    Components
    // wires/UserDashboard.bx
    class extends="cbwire.models.Component" {
        data = {
            "users": [],
            "searchTerm": ""
        };
    
        function onMount() {
            data.users = getUserService().getAllUsers();
        }
    
        function filteredUsers() computed {
            return data.users.filter(function(user) {
                return !data.searchTerm.len() ||
                       user.name.findNoCase(data.searchTerm);
            });
        }
    }
    // wires/UserDashboard.cfc
    component extends="cbwire.models.Component" {
        data = {
            "users" = [],
            "searchTerm" = ""
        };
    
        function onMount() {
            data.users = getUserService().getAllUsers();
        }
    
        function filteredUsers() computed {
            return arrayFilter(data.users, function(user) {
                return !len(data.searchTerm) ||
                       findNoCase(data.searchTerm, user.name);
            });
        }
    }
    // wires/PhotoUpload.bx
    class extends="cbwire.models.Component" {
        data = {
            "photo": "",
            "uploadFailed": false,
            "errorMessage": ""
        };
    
        function onUploadError( property, errors, multiple ) {
            data.uploadFailed = true;
            data.errorMessage = "Failed to upload " & ( multiple ? "files" : "file" );
    
            if ( !isNull( errors ) ) {
                writeLog( type="error", text="Upload error for #property#: #serializeJSON(errors)#" );
            }
        }
    
        function save() {
            if ( data.photo != "" ) {
                var uploadPath = expandPath( "./uploads/#createUUID()#.jpg" );
                fileWrite( uploadPath, data.photo.get() );
                data.photo.destroy();
                data.photo = "";
                data.uploadFailed = false;
            }
        }
    }
    // wires/PhotoUpload.cfc
    component extends="cbwire.models.Component" {
        data = {
            "photo" = "",
            "uploadFailed" = false,
            "errorMessage" = ""
        };
    
        function onUploadError( property, errors, multiple ) {
            data.uploadFailed = true;
            data.errorMessage = "Failed to upload " & ( multiple ? "files" : "file" );
    
            if ( !isNull( errors ) ) {
                writeLog( type="error", text="Upload error for #property#: #serializeJSON(errors)#" );
            }
        }
    
        function save() {
            if ( data.photo != "" ) {
                var uploadPath = expandPath( "./uploads/#createUUID()#.jpg" );
                fileWrite( uploadPath, data.photo.get() );
                data.photo.destroy();
                data.photo = "";
                data.uploadFailed = false;
            }
        }
    }
    <!-- wires/photoUpload.bxm -->
    <bx:output>
    <div>
        <h1>Upload Photo</h1>
    
        <form wire:submit.prevent="save">
            <input type="file" wire:model="photo" accept="image/*">
    
            <bx:if uploadFailed>
                <div class="alert alert-danger">
                    #errorMessage#
                </div>
            </bx:if>
    
            <button type="submit">Save Photo</button>
        </form>
    </div>
    </bx:output>
    <!-- wires/photoUpload.cfm -->
    <cfoutput>
    <div>
        <h1>Upload Photo</h1>
    
        <form wire:submit.prevent="save">
            <input type="file" wire:model="photo" accept="image/*">
    
            <cfif uploadFailed>
                <div class="alert alert-danger">
                    #errorMessage#
                </div>
            </cfif>
    
            <button type="submit">Save Photo</button>
        </form>
    </div>
    </cfoutput>
    <!-- wires/navigation.bxm -->
    <bx:output>
    <nav>
        <ul>
            <li><a href="#event.buildLink('dashboard')#">Dashboard</a></li>
            <li><a href="#event.buildLink('profile')#">Profile</a></li>
            <li><a href="#event.buildLink('settings')#">Settings</a></li>
        </ul>
    </nav>
    </bx:output>
    <!-- wires/navigation.cfm -->
    <cfoutput>
    <nav>
        <ul>
            <li><a href="#event.buildLink('dashboard')#">Dashboard</a></li>
            <li><a href="#event.buildLink('profile')#">Profile</a></li>
            <li><a href="#event.buildLink('settings')#">Settings</a></li>
        </ul>
    </nav>
    </cfoutput>
    // wires/UserProfile.bx
    class extends="cbwire.models.Component" {
        data = {
            "user": {
                "name": {
                    "first": "John",
                    "last": "Doe"
                },
                "contact": {
                    "email": "[email protected]",
                    "phone": "555-1234"
                }
            }
        };
    
        function onUpdateuser_name_first( value, oldValue ) {
            writeLog( "First name changed from #oldValue# to #value#" );
        }
    }
    // wires/UserProfile.cfc
    component extends="cbwire.models.Component" {
        data = {
            "user" = {
                "name" = {
                    "first" = "John",
                    "last" = "Doe"
                },
                "contact" = {
                    "email" = "[email protected]",
                    "phone" = "555-1234"
                }
            }
        };
    
        function onUpdateuser_name_first( value, oldValue ) {
            writeLog( "First name changed from #oldValue# to #value#" );
        }
    }
    <!-- wires/userProfile.bxm -->
    <bx:output>
    <div>
        <input type="text" wire:model="user.name.first" placeholder="First Name">
        <input type="text" wire:model="user.name.last" placeholder="Last Name">
        <input type="email" wire:model="user.contact.email" placeholder="Email">
        <input type="tel" wire:model="user.contact.phone" placeholder="Phone">
    </div>
    </bx:output>
    <!-- wires/userProfile.cfm -->
    <cfoutput>
    <div>
        <input type="text" wire:model="user.name.first" placeholder="First Name">
        <input type="text" wire:model="user.name.last" placeholder="Last Name">
        <input type="email" wire:model="user.contact.email" placeholder="Email">
        <input type="tel" wire:model="user.contact.phone" placeholder="Phone">
    </div>
    </cfoutput>
    moduleSettings = {
        cbwire = {
            csrfEnabled = true, // Enabled by default
            csrfStorage = "SessionCSRFStorage@cbwire"
        }
    };
    // wires/PhotoUpload.bx
    class extends="cbwire.models.Component" {
        data = {
            "photo": ""
        };
    
        function save() {
            if (data.photo != "") {
                // New in 5.0: Use store() to move file to permanent location
                var storedPath = data.photo.store("/uploads/photos");
    
                // Process the stored file
                writeLog("Photo stored at: #storedPath#");
    
                // Clean up
                data.photo.destroy();
                data.photo = "";
            }
        }
    }
    // wires/PhotoUpload.cfc
    component extends="cbwire.models.Component" {
        data = {
            "photo" = ""
        };
    
        function save() {
            if (data.photo != "") {
                // New in 5.0: Use store() to move file to permanent location
                var storedPath = data.photo.store("/uploads/photos");
    
                // Process the stored file
                writeLog("Photo stored at: #storedPath#");
    
                // Clean up
                data.photo.destroy();
                data.photo = "";
            }
        }
    }
    // ./wires/ShowPost.bx
    class extends="cbwire.models.Component" {
        data = {
            "title": "",
            "author": ""
        };
    
        function onMount( params ) {
            // Now required: explicitly set parameters when onMount() is defined
            data.title = params.title;
            data.author = params.author;
        }
    }
    // ./wires/ShowPost.cfc
    component extends="cbwire.models.Component" {
        data = {
            "title" = "",
            "author" = ""
        };
    
        function onMount( params ) {
            // Now required: explicitly set parameters when onMount() is defined
            data.title = params.title;
            data.author = params.author;
        }
    }

    File Uploads

    Handle file uploads with automatic temporary storage, validation, and processing. CBWIRE automatically converts uploaded files into FileUpload objects with methods for accessing content, metadata, and preview URLs.

    Basic Usage

    Create a photo upload component with save functionality:

    How It Works

    CBWIRE handles file uploads through a multi-step process:

    1. Request signed URL - CBWIRE gets a temporary upload URL from the server

    2. Upload file - JavaScript uploads the file to secure temporary storage

    3. Create FileUpload object - The data property becomes a FileUpload instance

    4. Process file - Use FileUpload methods to validate, manipulate, or store the file

    5. Store permanently - Move files to permanent storage using the store() method

    By default, CBWIRE stores uploaded files in the system's temporary directory (via getTempDirectory()) for enhanced security. This prevents unauthorized access to uploaded files before they're explicitly moved to permanent storage. You can configure a custom temporary storage path using the uploadsStoragePath setting. This is useful for distributed server environments where temporary files need to be shared across multiple servers. See the Configuration documentation for details.

    FileUpload Methods

    When a file is uploaded, CBWIRE creates a FileUpload object with the following methods:

    File Content

    Method
    Description

    get()

    Returns the binary content of the uploaded file

    getBase64()

    Returns base64 encoded string of the file

    getBase64Src()

    Returns base64 data URL for use in <img> tags

    File Information

    Method
    Description

    getSize()

    Returns file size in bytes

    getMimeType()

    Returns the MIME type of the file

    getMeta()

    Returns all metadata about the uploaded file

    isImage()

    Returns true if the file is an image

    File Locations

    Method
    Description

    getTemporaryStoragePath()

    Returns the temporary file storage path

    getMetaPath()

    Returns path to file metadata

    getPreviewURL()

    Returns URL for previewing images

    File Storage

    Method
    Description

    store( path )

    Moves file from temporary to permanent storage. Path can be a directory or full file path. Creates destination directories automatically. Returns absolute path to stored file.

    Cleanup

    Method
    Description

    destroy()

    Deletes temporary file and metadata (call after saving)

    Always call destroy() on FileUpload objects after saving to permanent storage to clean up temporary files.

    Image previews using getPreviewURL() only work with image file types. Use isImage() to check before displaying previews.

    Storing Files Permanently

    The store() method moves uploaded files from temporary storage to a permanent location. This is the recommended approach for persisting uploaded files.

    Basic Usage

    Pass a directory path to store the file with its original filename:

    Store with Custom Filename

    Pass a full file path to store with a custom filename:

    Automatic Directory Creation

    The store() method automatically creates destination directories if they don't exist:

    The store() method is more efficient than reading file content with get() and writing it manually. It moves the file directly using fileMove(), avoiding the overhead of reading and writing file content.

    After calling store(), the FileUpload object's internal path is updated to the new location. You can continue to use FileUpload methods like get() or getSize() on the stored file. However, you should call destroy() after storing to clean up the metadata file in temporary storage.

    Loading States

    Display upload progress using wire:loading directives:

    Handling Upload Errors

    The onUploadError() lifecycle hook is automatically called when a file upload fails with any HTTP response outside the 2xx range. This allows you to handle upload failures gracefully and provide feedback to users.

    Method Signature

    Parameter
    Type
    Description

    property

    string

    The name of the data property associated with the file input

    errors

    any

    The error response from the server. Will be null unless the HTTP status is 422, in which case it contains the response body

    multiple

    boolean

    Indicates whether multiple files were being uploaded (true) or a single file (false)

    Usage Example

    When It's Called

    The onUploadError() hook is triggered when:

    • The upload server returns any HTTP status code outside the 2xx range (200-299)

    • Network errors occur during upload

    • The server is unreachable

    • Any other upload failure scenario

    Error Information

    The errors parameter provides different information based on the HTTP status:

    • 422 (Unprocessable Entity): Contains the full response body (typically validation errors)

    • Other error codes: Will be null

    You can check the response status in your server logs or implement custom error handling based on your application's needs.

    The onUploadError() hook is optional. If not defined, upload errors will be handled silently by the JavaScript error callback. The hook is called AFTER the upload:errored event is dispatched to the JavaScript layer. All three parameters are always provided, though errors may be null.

    // wires/PhotoUpload.bx
    class extends="cbwire.models.Component" {
        data = {
            "photo": "",
            "isUploading": false
        };
    
        function save() {
            if (data.photo != "") {
                // Move file to permanent location
                var storedPath = data.photo.store(expandPath("./uploads"));
    
                // Clean up temporary file
                data.photo.destroy();
    
                // Reset form
                data.photo = "";
            }
        }
    }
    // wires/PhotoUpload.cfc
    component extends="cbwire.models.Component" {
        data = {
            "photo" = "",
            "isUploading" = false
        };
    
        function save() {
            if (data.photo != "") {
                // Move file to permanent location
                var storedPath = data.photo.store(expandPath("./uploads"));
    
                // Clean up temporary file
                data.photo.destroy();
    
                // Reset form
                data.photo = "";
            }
        }
    }
    function save() {
        if (data.photo != "") {
            // Store in directory with original filename
            var storedPath = data.photo.store(expandPath("./uploads"));
    
            writeLog("File stored at: #storedPath#");
    
            data.photo.destroy();
        }
    }
    function save() {
        if (data.photo != "") {
            // Store in directory with original filename
            var storedPath = data.photo.store(expandPath("./uploads"));
    
            writeLog("File stored at: #storedPath#");
    
            data.photo.destroy();
        }
    }
    function save() {
        if (data.document != "") {
            // Store with custom filename
            var customPath = expandPath("./uploads/#createUUID()#.pdf");
            var storedPath = data.document.store(customPath);
    
            // Save path to database
            saveDocumentPath(storedPath);
    
            data.document.destroy();
        }
    }
    function save() {
        if (data.document != "") {
            // Store with custom filename
            var customPath = expandPath("./uploads/#createUUID()#.pdf");
            var storedPath = data.document.store(customPath);
    
            // Save path to database
            saveDocumentPath(storedPath);
    
            data.document.destroy();
        }
    }
    function save() {
        if (data.avatar != "") {
            // Directory will be created if it doesn't exist
            var userUploadDir = expandPath("./uploads/users/#data.userId#");
            var storedPath = data.avatar.store(userUploadDir);
    
            data.avatar.destroy();
        }
    }
    function save() {
        if (data.avatar != "") {
            // Directory will be created if it doesn't exist
            var userUploadDir = expandPath("./uploads/users/#data.userId#");
            var storedPath = data.avatar.store(userUploadDir);
    
            data.avatar.destroy();
        }
    }
    // wires/PhotoUpload.bx
    class extends="cbwire.models.Component" {
        data = {
            "photo": "",
            "uploadFailed": false,
            "errorMessage": ""
        };
    
        function onUploadError( property, errors, multiple ) {
            // Set error state
            data.uploadFailed = true;
    
            // Create user-friendly error message
            data.errorMessage = "Failed to upload " & ( multiple ? "files" : "file" ) & " for " & property;
    
            // Log the error for debugging
            if ( !isNull( errors ) ) {
                writeLog( type="error", text="Upload error for #property#: #serializeJSON(errors)#" );
            }
        }
    }
    // wires/PhotoUpload.cfc
    component extends="cbwire.models.Component" {
        data = {
            "photo" = "",
            "uploadFailed" = false,
            "errorMessage" = ""
        };
    
        function onUploadError( property, errors, multiple ) {
            // Set error state
            data.uploadFailed = true;
    
            // Create user-friendly error message
            data.errorMessage = "Failed to upload " & ( multiple ? "files" : "file" ) & " for " & property;
    
            // Log the error for debugging
            if ( !isNull( errors ) ) {
                writeLog( type="error", text="Upload error for #property#: #serializeJSON(errors)#" );
            }
        }
    }
    <!-- wires/photoUpload.bxm -->
    <bx:output>
    <div>
        <input type="file" wire:model="photo">
    
        <bx:if uploadFailed>
            <div class="alert alert-danger">
                #errorMessage#
            </div>
        </bx:if>
    </div>
    </bx:output>
    <!-- wires/photoUpload.cfm -->
    <cfoutput>
    <div>
        <input type="file" wire:model="photo">
    
        <cfif uploadFailed>
            <div class="alert alert-danger">
                #errorMessage#
            </div>
        </cfif>
    </div>
    </cfoutput>
    <!-- wires/photoUpload.bxm -->
    <bx:output>
    <div>
        <h1>Upload Photo</h1>
        
        <form wire:submit.prevent="save">
            <div>
                <label for="photo">Select Photo:</label>
                <input type="file" 
                       id="photo" 
                       wire:model="photo" 
                       accept="image/*">
            </div>
            
            <!-- Loading indicator -->
            <div wire:loading wire:target="photo">
                Uploading photo...
            </div>
            
            <!-- Preview uploaded photo -->
            <bx:if photo != "" AND photo.isImage()>
                <div class="preview">
                    <h3>Preview:</h3>
                    <img src="#photo.getPreviewURL()#" 
                         alt="Uploaded photo" 
                         style="max-width: 200px;">
                    <p>Size: #photo.getSize()# bytes</p>
                </div>
            </bx:if>
            
            <button type="submit" 
                    wire:loading.attr="disabled">
                Save Photo
            </button>
        </form>
    </div>
    </bx:output>
    <!-- wires/photoUpload.cfm -->
    <cfoutput>
    <div>
        <h1>Upload Photo</h1>
        
        <form wire:submit.prevent="save">
            <div>
                <label for="photo">Select Photo:</label>
                <input type="file" 
                       id="photo" 
                       wire:model="photo" 
                       accept="image/*">
            </div>
            
            <!-- Loading indicator -->
            <div wire:loading wire:target="photo">
                Uploading photo...
            </div>
            
            <!-- Preview uploaded photo -->
            <cfif photo != "" AND photo.isImage()>
                <div class="preview">
                    <h3>Preview:</h3>
                    <img src="#photo.getPreviewURL()#" 
                         alt="Uploaded photo" 
                         style="max-width: 200px;">
                    <p>Size: #photo.getSize()# bytes</p>
                </div>
            </cfif>
            
            <button type="submit" 
                    wire:loading.attr="disabled">
                Save Photo
            </button>
        </form>
    </div>
    </cfoutput>
    <input type="file" wire:model="document">
    
    <div wire:loading wire:target="document">
        Uploading document...
    </div>
    
    <button type="submit" wire:loading.attr="disabled">
        Save Document
    </button>
    function onUploadError( property, errors, multiple )
    FORGEBOX: CBWIRE CLIwww.forgebox.io
    CBWIRE CLI on ForgeBox
    Logo