Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Coming soon.
Coming soon
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.
The resources below are dated, and CBWIRE has dramatically changed since its release.
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.
To use wire:cloak, add the directive to any element you want to hide during page load:
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.
Lazy loading also delays the execution of the onMount() lifecycle method.
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.
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.
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
}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.
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.
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.
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
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.
By default, wire:current removes trailing slashes (/) from its comparison. Use the .strict modifier to enforce strict path string comparison:
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
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.
CBWIRE consists of four main parts working together:
Server-side Components - Your BoxLang/CFML classes that handle data and business logic
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.
Let's create a reactive counter component to demonstrate CBWIRE's power. Download and install , then run these commands:
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.
Create a search component that updates the URL as users type:
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.
This contact form demonstrates wire:dirty with unsaved change indicators and targeted property tracking:
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.
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>When users visit /articles?search=javascript&category=tech, the component automatically initializes with those values and performs the search.
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@endWireComponents can now be organized within ColdBox modules.
CBWIRE no longer requires cbValidation, making it more lightweight.
Templates now use direct property access instead of the args scope:
Skip unnecessary ColdBox view caching/lookups
Streamlined engine architecture
Removed computedPropertiesProxy for better performance
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
<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
Let's trace through what happens when a CBWIRE component is rendered and interacted with:
When a user first requests a page containing CBWIRE components:
Component Instantiation: CBWIRE creates an instance of your component class
Data Initialization: The data struct is populated with default values
Mount Lifecycle: If present, the onMount() method executes
Template Rendering: Your component's template is rendered with current data values
HTML Generation: The final HTML is sent to the browser with embedded metadata
When a user interacts with your component (clicks a button, submits a form, etc.):
Event Capture: Livewire.js intercepts the DOM event (click, input change, form submit)
Request Preparation: The client serializes current component state and the action to perform
Server Request: An AJAX request is sent to CBWIRE's update endpoint
Component Hydration: CBWIRE recreates your component instance from the serialized state
Action Execution: Your component method runs with the current data context
Data Updates: Component data properties are modified by your action method
Re-rendering: The template is re-rendered with updated data
Response Generation: New HTML and updated state are sent back to the client
DOM Diffing: Livewire.js compares old and new HTML, updating only changed elements
UI Update: The page updates without a full refresh
Here's what a typical CBWIRE request looks like:
CBWIRE provides several ways to bind data between your template and component:
Data flows from component to template automatically:
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.
Actions are public methods in your component that can be called from the template:
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.
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.
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:
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.
CBWIRE includes several security features:
Optional CSRF tokens prevent cross-site request forgery
Tokens are automatically managed when enabled
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
Browser developer tools show CBWIRE requests
Server logs contain component errors
ColdBox debugger shows component lifecycle
Components can communicate through events:
Combine CBWIRE with Alpine.js for optimal UX:
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.
Insert a counter component into your Main layout using wire("Counter"):
Define your Counter component:
Define the counter template:
Navigate to your application in the browser. Click the + and - buttons to see the counter update instantly without page refreshes or JavaScript code! 🤯
CBWIRE seamlessly bridges your server-side BoxLang or CFML code with dynamic frontend behavior:
Initial Render: CBWIRE renders your component with its default data values
User Interaction: When a user clicks a button with wire:click, Livewire.js captures the event
Server Request: The interaction triggers an AJAX request to your CBWIRE component's action method
Data Processing: Your server-side action method processes the request and updates component data
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.
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
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.
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
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.
Please consider becoming one of our Patreon supporters to help us continue developing and improving CBWIRE.

<!-- 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">
<
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
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"
// 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
}
}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"
Set a data property to the specified value.
Set a computed property to the specified closure.
Toggles a data property between true and false.
Calls an action. An optional array of parameters can be provided.
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.
Verifies a value can be found in the current component rendering. Otherwise, the test fails.
Verifies a value is not found in the current component rendering. Otherwise, the test fails.
Verifies a data property matches a specified value. Otherwise, the test fails.
Verifies a data property does not match a specified value. Otherwise, test fails.
<!-- 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
You can also instruct Livewire to replace the target element as well as all children with wire:replace.self.
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.
Install via CommandBox like so:
Create a simple component:
Create a component with data properties and actions:
Create a component in a specific module:
Generate a component with comprehensive features:
Create a single-file component with all features:
Generate a component optimized for lazy loading:
12/20/2023
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.
Support for wire("MyComponent@module") calls enables loading components from specific modules, improving organization in complex applications.
CBWIRE 3.2 adds support for dispatching browser events using the new dispatch() method, enabling better integration with JavaScript frameworks and third-party libraries.
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
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
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
This release maintains backward compatibility with existing CBWIRE 3.x applications. No breaking changes were introduced in version 3.2.
Special thanks to Michael Rigsby for contributing the auto-populate data properties feature and module-specific component loading functionality.
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.
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 = {
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
The wire:init directive does not support any modifiers.
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.
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() {
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
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"
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.
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++;
}
}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
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
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.
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.
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.
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 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:
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.
This simple component demonstrates wire:offline for showing content when offline, adding CSS classes, and removing CSS classes:
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.
This simple component demonstrates wire:poll with default intervals, custom timing, background polling, and viewport polling:
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.
This simple list demonstrates wire:key for proper DOM tracking in loops:
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.
This contact form demonstrates wire:submit with form handling and loading states:
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.
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:clickfunction 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_jdkwire( "TaskList" )
.data( "tasks", [ "task1", "task2" ] ) // one property at a time
.data( { "tasks" : [ "task1", "task2" ] } ); // struct of propertieswire( "TaskList" )
.computed( "count", function() { return 3; } ) // one property at a time
.computed( { "count" : function() { return 3; } } ); // struct of propertieswire( "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
<!-- 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>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
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");
}
}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
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.
// 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;
});
}
}// 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");
}
}// 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;
}
}All string data properties are now automatically trimmed of whitespace, ensuring cleaner data handling.
Components can now be accessed directly from JavaScript using the global cbwire.find() method.
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.
The reset() method can now reset all data properties when called without parameters.
Updated lifecycle method naming for consistency - mount() is now onMount().
Faster hydration process with optimized component restoration
Better memory management for long-running applications
Streamlined event handling and data binding
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
Fixed DocBox integration: Resolved documentation generation issues caused by file structure changes
Better error handling: Improved error messages and debugging information
Hydration timing: Ensured proper order of hydration lifecycle methods
State restoration: Fixed issues with component state not being properly restored in certain scenarios
Update component lifecycle methods from mount() to onMount():
This change provides better consistency with other lifecycle methods and clearer naming conventions.
CBWIRE uses automatic file matching for components and templates:
Templates must have a single outer element for proper DOM binding:
Access component data properties directly by name:
Define computed properties with the computed annotation and call them as methods:
Override default template location using the onRender() method:
Access global helper methods from installed ColdBox modules:
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>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:
click
wire:click
keydown
wire:keydown
submit
wire:submit
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.
You can pass parameters to actions such as actionName( arg1, arg2, arg3 ).
The parameter is then passed through to your actions via function arguments.
There are a few magic actions already created on your components.
$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.
// 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"
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
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.
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;
}
You can access Computed Properties in your component template using propertyName().
You can also access Computed Properties from within your Actions.
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.
// wires/ContactForm.cfc
component extends="cbwire.models.Component" {
data = {
"name" = "",
"email" = ""
};
function submit() {
// Process form (simulate processing)
sleep(1000);
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
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 = "";
}
}// 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.
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.
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.
You can render a component using the wire() method.
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.
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.
Parameters are passed into your component's method. This is an optional method you can add.
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.
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.
CBWIRE provides lifecycle methods you can hook into to update and render your components.
The lifecycle methods are executed in the following order when a component is initially loaded:
onSecure()
onMount()
onRender()
Lifecycle methods are executed in this order for subsequent AJAX requests.
onSecure()
onHydrate[DataProperty]()
onHydrate()
onUpdate[DataProperty]()
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.
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.
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.
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.
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.
Runs on subsequent requests after any is updated using wire:model or $set.
Runs on subsequent requests after a is updated using wire:model or $set. It only runs when the targeted data property is updated.
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.
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
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.
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.
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.
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() {
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
The wire:click directive supports one modifier:
Prevent: .prevent - Prevents default browser behavior (essential for links)
Example: wire:click.prevent="sendEmail"
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.
Before installing CBWIRE, ensure your system meets these requirements:
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.
This form demonstrates wire:loading with various loading states and targeting options:
cbwire create wire myComponentcbwire create wire name="userProfile" dataProps="name,email,isActive" actions="save,cancel" --opencbwire 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 --opencbwire create wire name="taskList" \
dataProps="tasks,newTask,filter" \
lockedDataProps="tasks" \
actions="addTask,toggleTask,removeTask" \
outerElement="section" \
lifeCycleEvents="onMount,onRender" \
--singleFileWire --jsWireRef --opencbwire 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>Provides Loading States: Integrates with wire:loading for visual feedback during actions
// 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>
// ./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;
}
}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)
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;
}
}// wires/MyComponent.bx
class extends="cbwire.models.Component" {
// Data properties
data = {
"name": ""
};
}// wires/MyComponent.cfc
component extends="cbwire.models.Component" {
// Data properties
data = {
"name": ""
};
}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
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)
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.
// 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 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
Adobe ColdFusion 2023+ or Adobe ColdFusion 2025+
Lucee 5.3+ or Lucee 6.0+
ColdBox 6+
CBWIRE is distributed through ForgeBox and can be installed using CommandBox.
If you don't have CommandBox installed, download it from ortussolutions.com/products/commandbox.
Navigate to your ColdBox application root and run:
For the latest development version:
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:
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.
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.
If you prefer to control when and how assets are loaded, you can disable automatic injection:
Then manually include the assets in your layout:
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
If you encounter issues:
Verify assets are loaded: Check your browser's developer tools to ensure CBWIRE CSS and JavaScript are present
Check server logs: Look for any CBWIRE-related errors in your application logs
Validate requirements: Ensure all system requirements are met, especially for BoxLang applications
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.
11/26/2022
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
Fixed empty return values: Computed properties that return no value no longer cause errors
Better error handling: Improved error messages when computed properties fail
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
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
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
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
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
Clearer error messages for rendering issues
Better debugging information for failed component initialization
Improved development workflow for simple components
More reliable template resolution
Better handling of edge cases in component nesting
Improved variable scope management
This release maintains full backward compatibility with existing CBWIRE 2.0 applications. No breaking changes were introduced in version 2.1.
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.
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
};
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
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
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.
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.
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.
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 .
You can send parameters as your dispatching an event.
You can dispatch events to components of a specific component using dispatchTo().
You can dispatch events to the component that fired the event using dispatchSelf().
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.
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.
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.
// wires/UserDashboard.bx
class extends="cbwire.models.Component" {
data = {
"isLoggedIn": false
};
function logout() {
// Perform logout logic
data
Redirect to pages within your application:
Redirect to external websites:
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.
Redirects immediately terminate action execution. Any code after a redirect() call will not execute.
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:
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.
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@5box 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>.scaleOrigins: .origin.top|bottom|left|right (scale origin point)
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:
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
Defines the directory where your CBWIRE components are stored, relative to your application root. This affects component auto-discovery and naming conventions.
Default: wires
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.
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
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
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)
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.
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.
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
Customizes the color of the progress bar displayed during wire:navigate operations. Accepts any valid CSS color value.
Default: #2299dd
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
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.
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
// 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
}
};
}
}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.)
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"
Don't initialize textarea content with the property value in the template:
The checkbox will be checked if the data property is true, unchecked if false.
Livewire automatically adds the selected attribute to the matching option.
Use wire:key to ensure dependent dropdowns update properly:
// 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">×</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>05/16/2024
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 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.
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.
Execute JavaScript directly from your component actions using the new js() method, enabling seamless server-to-client communication.
Transform your application into a single-page application using the new wire:navigate directive for seamless navigation without full page reloads.
Stream content in real-time using the new wire:stream directive for dynamic content updates.
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
Optimize performance with intelligent request bundling that reduces the number of server requests by combining multiple component updates into single requests.
CBWIRE 4.0 features a completely rewritten engine architecture providing:
Improved performance and reliability
Better component lifecycle management
Enhanced debugging capabilities
Modern development patterns
Updated lifecycle methods for better consistency and clarity:
mount() renamed to onMount()
updated() renamed to onUpdate()
Streamlined configuration by removing deprecated settings:
Removed enableTurbo global setting (superseded by wire:navigate)
Removed throwOnMissingSetter global setting (improved error handling)
Faster component rendering and updates
Optimized request handling with bundling
Improved memory usage and garbage collection
Enhanced component lifecycle management
If you're upgrading from CBWIRE 3.x, update your lifecycle methods:
Remove deprecated settings from your configuration:
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.
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
For cases where you need to retrieve dependencies dynamically within action methods, use the getInstance() method:
The getInstance() method provides flexible dependency retrieval:
CBWIRE components fully participate in WireBox's lifecycle management. All WireBox lifecycle methods are available:
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.
Learn about the full capabilities of WireBox at .
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.
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(
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
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.
Here's what happens during navigation:
User clicks a wire:navigate link
Livewire prevents default browser navigation
Loading bar appears at top of page
Page content is fetched in background
Use CBWIRE's redirect() method with navigation enabled:
Use Alpine's x-persist to maintain elements like audio/video players:
Livewire preserves page scroll by default. For specific elements, use wire:scroll:
Navigation triggers three lifecycle events:
Trigger navigation programmatically:
Handle persistent event listeners properly:
Ensure analytics track navigation properly:
Control script execution across navigations:
Configure the loading indicator:
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.
// 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#");
}
}.scaleOrigins: .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
Performance Boost: Often achieves 2x faster page loads than traditional navigation
New content replaces current page seamlessly
Write comprehensive unit tests for your CBWIRE components with the new testing framework.
Override the default wires folder location to organize components according to your application structure.
Prevent template rendering when you only need to update data without returning HTML.
Track which properties have changed since the last render for optimized updates.
Computed properties now run only once per request during rendering for better performance.
Components now support ColdBox's dependency injection system.
Enable single-page application functionality with improved Turbo integration.
Set custom template paths for components using the this.template property.
Data properties now properly support null as a valid value.
Updated to Livewire JS v2.10.6 for improved client-side functionality and bug fixes.
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
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
Fixed back button issues: Browser back button now works correctly with CBWIRE components
Improved navigation: Better handling of browser history and navigation states
Parameter validation: Ensured params is properly passed as an array
Method parameter handling: Fixed missing parameters in component method calls
This is a major version release with some breaking changes:
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 requests now require the X-Livewire header. This improves security but may affect custom AJAX implementations.
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
}
}09/29/2024
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.
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.
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.
Configure custom module root URLs for better control over CBWIRE routing in complex applications.
New getTemporaryStoragePath() method in FileUpload components provides access to temporary file storage locations.
Enhanced progress bar configuration with proper color setting and disable functionality.
Automatically trim whitespace from string values in form inputs.
Load CBWIRE components from external module locations for better code organization.
Customize the CBWIRE update endpoint for advanced routing scenarios.
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
CBWIRE rendering speed improvements
Enhanced child/nested component tracking
Optimized asset management and injection
Fixed onRender() method: Restored missing onRender() method from previous versions
Fixed onUpdate() behavior: Resolved issue where onUpdate() was called when no updates occurred
Child/nested component tracking: Improved tracking of child components in Livewire
Component isolation: Better handling of component isolation and lazy loading
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
Module loading: Fixed issues loading wires from external module locations
Path resolution: Improved component path resolution for complex module structures
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.
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.
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:
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.
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.
The X-Livewire header is essential for CBWIRE to function properly. If you're experiencing 400 errors with other server configurations:
Check server logs - Look for messages about rejected or stripped headers
Review proxy settings - Ensure reverse proxies (nginx, Apache) pass through the X-Livewire header
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.
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.
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.
Placeholder-Template Matching: Outer elements must be identical
Conditional Blocks: Wrap complex conditionals with <!--[if BLOCK]><![endif]--> markers
Alpine Quotes: Always use single quotes inside x-data attributes
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 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.
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;
}
}X-Livewirex-show instead of x-if with CBWIRE componentsx-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>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.
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.
When rendering multiple nested components dynamically, use unique keys to help Livewire track each component individually:
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
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.
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:
You can execute bespoke JavaScript in your CBWIRE component using <cbwire:script>.
Here's a complete example:
Unlike assets, which are loaded only once per page, scripts are evaluated for every component instance.
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:
You can access your component's $wire object within your scripts block.
You can access the livewire:init and livewire:initialized events as Livewire is loading.
You can interact with Livewire from external scripts using the Livewire global object.
Livewire allows you to register custom directives using Livewire.directive().
Here's the implementation for wire:confirm:
A commit is made every time a CBWIRE component is sent to the server. You can hook into an individual commit like this:
Using the request hook, you can hook into the entire HTTP request, which may contain multiple commits.
If the default page expiration dialog isn't suitable for your application, you can use the request hook to implement a custom solution.
// 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>09/25/2023
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.
New onUpdate() and onUpdateProperty() lifecycle methods provide granular control over component updates.
Parent components can now refresh all child components using the new refresh functionality.
Components can now call CBWIRE User Defined Functions directly from templates, expanding beyond computed properties.
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.
Support for params as an alias for parameters in onMount() method calls.
Templates can now access ColdBox application helpers and module helpers directly.
New resetExcept() method allows resetting all data properties except specified ones.
Faster rendering with optimized view caching and lookups
Improved single file component compilation and caching
Better memory management for high-traffic applications
Removed unnecessary template variables to prevent collisions
Cleaner template scope with better variable isolation
Improved debugging experience
Fixed child components not re-rendering on follow-up requests
Better tracking and lifecycle management for nested components
Improved parent-child communication
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
Fixed file name collisions: Improved handling of SFC file names in high-traffic applications
Automatic cleanup: Compiled SFCs are now properly cleared on fwreinit
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.
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.
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 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.
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.
Using the data structure, you can access data properties from within your component actions.
You can access data properties within your templates using #propertyName#.
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.
Reference nested properties using dot notation in wire:model and other directives:
Use dot notation to access and modify nested properties in your component methods:
Lifecycle hooks for nested properties use underscores instead of dots. A property path like user.name.first triggers onUpdateuser_name_first():
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 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.
Locked properties can be displayed but not modified through form inputs:
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
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" ] );
}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.
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.
When the cbSecurity module is installed and active, CBWIRE automatically integrates with it, enabling security annotations on components and methods.
Secure an entire component with the secured annotation:
Secure specific actions with annotations:
cbSecurity annotations support multiple formats:
CBWIRE fires the onCBWIRESecureFail interception point when security checks fail, allowing custom handling like logging or redirects.
Register the interceptor in your ColdBox configuration:
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.
When data travels between client and server, CBWIRE:
Calculates a cryptographic checksum of the component snapshot using HMAC-SHA256
Includes the checksum with the data payload
Validates the checksum on subsequent requests
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.
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.
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
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.
CSRF protection operates transparently:
CBWIRE generates a unique token when components mount
The token is included with every client-server interaction
Server validates the token before processing requests
Invalid or missing tokens reject the request
No changes to your component code are required—CSRF protection works automatically when enabled.
CSRF protection is enabled by default in CBWIRE 5.x. Configure it in your ColdBox settings:
CBWIRE provides two built-in storage implementations for CSRF tokens:
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
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
Implement custom CSRF storage (Redis, DynamoDB, etc.) by creating a component that implements the ICSRFStorage interface:
Configure your custom implementation:
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 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();
}
}Create a user registration form with validation:
// wires/UserRegistration.bx
class extends="cbwire.models.Component" {
data = {
"name": "",
"email": "",
"password": "",
Validates data and silently stops function execution if validation fails (does not throw an exception):
Returns a ValidationResult object for manual error handling:
Display all validation errors:
Display errors for specific fields:
Most frequently used validation constraints:
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
Define validation constraints inline:
Use requiredIf and requiredUnless for conditional validation:
For complete constraint documentation, see cbValidation documentation.
[Release Date TBD]
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
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.
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.
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.
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).
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.
Clearer error messages for empty component templates. Contributed by David Moreno.
Eliminated unnecessary "ioc.Injector" error messages during single file component initialization.
Enhanced snapshot deserialization and effects processing. Invalid JSON now returns empty struct instead of throwing errors. Improved HTML entity encoding for content with quotes.
Added locking mechanisms for thread-safe single file component building, preventing race conditions.
Load wire components from external module locations via modulesExternalLocation configuration. Reference using @module syntax: wire( name="MyComponent@externalModule" ).
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.
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.
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.
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+.
Added ColdBox 7+ and 8+ support. Supported versions: ColdBox 6+, 7+, 8+.
File uploads now respect the updateEndpoint configuration setting instead of using hardcoded /cbwire/upload path.
Fixed incorrect path generation in getUploadTempDirectory() method.
Fixed race condition when multiple requests create temporary directory simultaneously. Added proper locking mechanisms.
Fixed lazy loading attempting to call non-existent onMount() methods. Now checks for method existence before calling.
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 cbvalidationfunction 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 messageshasError("fieldName") // Returns true if field has errors
getError("fieldName") // Returns error message for fieldfunction 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()
{"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
// 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;
}
}CBWIRE handles file uploads through a multi-step process:
Request signed URL - CBWIRE gets a temporary upload URL from the server
Upload file - JavaScript uploads the file to secure temporary storage
Create FileUpload object - The data property becomes a FileUpload instance
Process file - Use FileUpload methods to validate, manipulate, or store the file
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.
When a file is uploaded, CBWIRE creates a FileUpload object with the following methods:
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
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
getTemporaryStoragePath()
Returns the temporary file storage path
getMetaPath()
Returns path to file metadata
getPreviewURL()
Returns URL for previewing images
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.
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.
The store() method moves uploaded files from temporary storage to a permanent location. This is the recommended approach for persisting uploaded files.
Pass a directory path to store the file with its original filename:
Pass a full file path to store with a custom filename:
The store() method automatically creates destination directories if they don't exist:
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.
Display upload progress using wire:loading directives:
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.
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)
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
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.
// 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 )