CxJS is a feature-rich JavaScript (TypeScript) framework for building complex web front-ends, such as portals, dashboards and admin applications. (Key articles only) # What is CxJS CxJS is a high-level TypeScript framework for building data-intensive web applications. Built on top of React, it provides widgets, forms, grids, charts, routing, and state management out of the box. ## Framework vs Library Unlike React, which is a library focused on rendering UI, CxJS is a full-featured framework. You don't need to search for compatible packages or worry about integration issues. Everything works together seamlessly. | React (Library) | CxJS (Framework) | | ------------------------------------- | ----------------------------- | | UI rendering only | Full application stack | | Choose your own router, forms, tables | Router, forms, grids included | | Integrate multiple packages | Single cohesive package | | Flexible, but requires decisions | Opinionated, but productive | ## Built for Business Applications CxJS is designed for rapid development of business applications that typically include: - **Forms** with validation, labels, and various input types - **Data tables** with sorting, filtering, grouping, and inline editing - **Charts** for data visualization - **Complex layouts** with navigation, tabs, and overlays If your application has many tables, forms, and charts, CxJS will significantly speed up your development. ```tsx import { Controller, createModel, enableCultureSensitiveFormatting, expr, tpl, } from "cx/ui"; import { Grid } from "cx/widgets"; enableCultureSensitiveFormatting(); interface SaleRecord { region: string; product: string; qty: number; revenue: number; } interface PageModel { sales: SaleRecord[]; $record: SaleRecord; $group: { region: string; productCount: number; }; } const m = createModel(); class PageController extends Controller { onInit() { this.store.set(m.sales, [ { region: "Europe", product: "Widget A", qty: 50, revenue: 2500 }, { region: "Europe", product: "Widget B", qty: 30, revenue: 1800 }, { region: "Europe", product: "Gadget X", qty: 20, revenue: 3200 }, { region: "Americas", product: "Widget A", qty: 80, revenue: 4000 }, { region: "Americas", product: "Widget B", qty: 45, revenue: 2700 }, { region: "Americas", product: "Gadget X", qty: 35, revenue: 5600 }, { region: "Asia", product: "Widget A", qty: 120, revenue: 6000 }, { region: "Asia", product: "Widget B", qty: 60, revenue: 3600 }, { region: "Asia", product: "Gadget X", qty: 40, revenue: 6400 }, ]); } } export default ( ); ``` ## Battle-Tested CxJS has been used in production for years, powering admin dashboards, business intelligence tools, data management applications, and internal enterprise tools. The framework is mature, stable, and continuously improved based on real-world usage. Beyond the rich widget library, CxJS offers declarative data binding, client-side routing, TypeScript-first development with full type safety, and theming support with multiple built-in themes. --- # Hello World Let's build a simple interactive example to see CxJS in action. ## Your First Example CxJS uses JSX syntax similar to React. Here's a basic component with a text field and data binding: ```tsx import { DateField, TextField } from "cx/widgets"; import { createModel, expr, hasValue, LabelsTopLayout } from "cx/ui"; import { dateDiff } from "cx/util"; interface PageModel { name: string; dob: string; } const m = createModel(); export default (

`Hello ${name ?? "World"}`)} />
{ if (!dob) return "-"; let diff = dateDiff(new Date(), new Date(dob)); let years = diff / (365.25 * 24 * 60 * 60 * 1000); return `You're approximately ${years.toFixed(1)} years old...`; })} />
); ``` Let's break down the key parts: - **PageModel** - TypeScript interface defining the shape of your data - **TextField**, **DateField** - CxJS form widgets for text and date input - **LabelsTopLayout** - A layout that positions labels above form fields - **expr()** - Creates a computed expression that updates when dependencies change - **hasValue()** - Returns true if the bound value is defined - **createModel** - Creates a typed accessor model for store bindings When the user enters their name or date of birth, the bindings update the store and the UI re-renders automatically. This declarative data binding is a core concept in CxJS—you define what data each widget uses, and the framework handles synchronization. --- # Installation CxJS is distributed as npm packages and works with modern build tools like Vite and webpack. ## Packages The main packages you'll need: | Package | Description | | ---------- | ----------------------------------------------------- | | `cx` | Core framework with widgets, charts, and data-binding | | `cx-react` | React integration for rendering | Install both packages: ```bash npm install cx cx-react ``` ### Themes CxJS includes several [themes](/intro/themes). Install one to get started: ```bash npm install cx-theme-aquamarine ``` ## TypeScript Configuration CxJS is written in TypeScript and provides full type definitions. Configure your `tsconfig.json`: ```json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "cx", "moduleResolution": "bundler", "esModuleInterop": true } } ``` The key setting is `jsxImportSource: "cx"` which enables CxJS-specific JSX types and attributes like `visible`, `controller`, `layout`, and data-binding functions. ## Build Configuration ### Vite Vite is the recommended build tool for new projects. Create `vite.config.ts`: ```typescript import { defineConfig } from "vite"; export default defineConfig({ esbuild: { jsxImportSource: "cx", }, }); ``` ### webpack For webpack projects, configure TypeScript and JSX handling: ```javascript module.exports = { module: { rules: [ { test: /\.(ts|tsx)$/, loader: "ts-loader", }, ], }, }; ``` For large applications, consider using `swc-loader` instead of `ts-loader` for faster builds: ```javascript module.exports = { module: { rules: [ { test: /\.(ts|tsx)$/, loader: "swc-loader", options: { jsc: { transform: { react: { runtime: "automatic", importSource: "cx", }, }, }, }, }, ], }, }; ``` ## Entry Point In your main entry file, import the theme and start the application: ```tsx import { startAppLoop } from "cx/ui"; import { Store } from "cx/data"; import "cx-theme-aquamarine/dist/index.css"; const store = new Store(); startAppLoop( document.getElementById("app"), store,

Welcome to CxJS

); ``` --- # Tailwind CSS CxJS works well with Tailwind CSS. Use Tailwind's utility classes for layout and custom styling while leveraging CxJS widgets for complex UI components like grids, forms, and charts. ## Using with CxJS Apply Tailwind classes directly to CxJS components using the `class` attribute: ```tsx import { TextField, Button } from "cx/widgets"; import { createModel } from "cx/ui"; interface PageModel { name: string; } const m = createModel(); export default (
); ``` Tailwind is particularly useful for: - **Page layouts** - Use flexbox and grid utilities - **Spacing** - Apply margin and padding with utility classes - **Custom components** - Style wrappers and containers around CxJS widgets CxJS themes handle widget internals (inputs, dropdowns, grids), while Tailwind handles the surrounding layout and custom elements. ## Installation Install Tailwind CSS and its dependencies: ```bash npm install tailwindcss postcss autoprefixer npx tailwindcss init -p ``` ## Configuration Configure `tailwind.config.js` to scan your source files: ```javascript /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], }; ``` Create a `tailwind.css` file with proper layer setup. Tailwind 4 uses CSS layers, and it's important to define a `cxjs` layer between `base` and `utilities` so CxJS styles have the correct specificity: ```css @layer theme, base, cxjs, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/preflight.css" layer(base); @import "tailwindcss/utilities.css" layer(utilities); ``` Then import your CxJS theme styles into the `cxjs` layer in your main stylesheet: ```css @import "./tailwind.css"; @layer cxjs { @import "cx-theme-aquamarine/src/variables"; @import "cx-theme-aquamarine/src/index"; } ``` ## Build Setup ### Vite Vite has built-in PostCSS support. After running `npx tailwindcss init -p`, it creates a `postcss.config.js` file that Vite picks up automatically: ```javascript export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ``` ### webpack For webpack, install the PostCSS loader: ```bash npm install postcss-loader ``` Add PostCSS to your CSS/SCSS rule: ```javascript module.exports = { module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader", "postcss-loader"], }, ], }, }; ``` ## Template For a complete example, see the [CxJS Tailwind CSS Template](https://github.com/codaxy/cxjs-tailwindcss-template) which includes a pre-configured webpack setup with layouts, dashboards, and sample pages. --- # JSX Syntax CxJS uses JSX to define user interfaces declaratively. If you're familiar with React, you'll find the syntax familiar — with some CxJS-specific extensions. ## Widgets vs HTML Elements CxJS follows a simple naming convention — widgets start with an uppercase letter (`Button`, `TextField`, `Grid`) while HTML elements start with a lowercase letter (`div`, `span`, `section`). You can freely mix them in the same component. CxJS widgets also support common attributes for controlling behavior and appearance. Use `visible` to conditionally render widgets, `class` or `className` for CSS classes, and `style` for inline styles: ```tsx import { createModel } from "cx/data"; import { Button, Switch, TextField } from "cx/widgets"; interface PageModel { name: string; showMessage: boolean; } const m = createModel(); export default (
Mixing Widgets and HTML
Visibility Control
Show message
This text is conditionally visible!
); ``` ## The `` Wrapper In earlier versions of CxJS, widget trees had to be wrapped in a `` element to instruct the Babel compiler to process them as CxJS configuration. With TypeScript and the new `jsxImportSource: "cx"` configuration, this wrapper is no longer required: ```tsx import { createModel } from "cx/data"; import { Button, TextField } from "cx/widgets"; interface PageModel { name: string; } const m = createModel(); export default (
); ``` The legacy `` syntax is still supported for backwards compatibility: ```tsx import { createModel } from "cx/data"; import { Button, TextField } from "cx/widgets"; interface PageModel { name: string; } const m = createModel(); export default (
); ``` ## Key Differences from React | Feature | React | CxJS | | ----------------- | ----------------- | ---------------------------- | | Class names | `className` only | `class` or `className` | | Two-way binding | Manual | Built-in via accessor chains | | State management | `useState`, Redux | Store with typed models | | Component wrapper | None | `` (optional) | --- # Typed Models CxJS uses **typed models** to provide type-safe access to data in the store. Instead of using string paths like `"user.firstName"`, you use accessor chains like `m.user.firstName` that are checked by TypeScript. ## Creating a Model Proxy Use `createModel()` to create a proxy object that mirrors your data structure. The proxy doesn't hold any data — it generates binding paths that connect widgets to the store. ```tsx import { createModel } from "cx/data"; import { TextField, Button } from "cx/widgets"; interface User { firstName: string; lastName: string; } interface PageModel { user: User; message: string; } const m = createModel(); export default (
Store content
 JSON.stringify(data, null, 2)} />
    
); ``` When you write `m.user.firstName`, CxJS creates a binding to the path `"user.firstName"` in the store. The TextField reads and writes to this path automatically. ## Why Typed Models? Typed models provide several benefits over string-based paths: - **Type safety** — TypeScript catches typos and invalid paths at compile time - **Autocomplete** — Your editor suggests available properties as you type - **Refactoring** — Rename a property and all usages update automatically - **Documentation** — Hover over a property to see its type ## Accessor Methods Accessor chains provide two useful methods for working with paths: | Method | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------- | | `toString()` | Returns the full string path represented by the accessor. Useful when you need to pass paths to APIs that expect strings. | | `nameOf()` | Returns only the last segment of the path (the property name). | ```tsx import { createModel } from "cx/data"; interface User { firstName: string; lastName: string; email: string; } interface PageModel { user: User; count: number; } const m = createModel(); export default (
Accessor Result
m.user.firstName.toString() "{m.user.firstName.toString()}"
m.user.email.toString() "{m.user.email.toString()}"
m.count.toString() "{m.count.toString()}"
m.user.lastName.nameOf() "{m.user.lastName.nameOf()}"
m.count.nameOf() "{m.count.nameOf()}"
); ``` ## Nested Structures Accessor chains work with deeply nested structures. Define your interfaces to match your data shape: ```tsx interface Address { street: string; city: string; country: string; } interface User { name: string; address: Address; } interface PageModel { user: User; } const m = createModel(); // Access nested properties m.user.address.city; // binds to "user.address.city" ``` The proxy automatically generates the correct path regardless of nesting depth. `createModel` can also be imported from `cx/ui`. `createAccessorModelProxy` is available as an alias for backward compatibility. --- # Store The **Store** is the central state container in CxJS. It holds all application data and notifies widgets when data changes, triggering automatic UI updates. ## Accessing the Store Every CxJS widget has access to the store through its instance. In event handlers, access it via the second parameter: ```tsx import { createModel, Store } from "cx/data"; import { Button } from "cx/widgets"; interface PageModel { count: number; } const m = createModel(); export default (
Store content
 JSON.stringify(data, null, 2)} />
    
); ``` The store is available in event handlers like `onClick`, `onChange`, and others. Use `store.get()` to read values and `store.set()` to write them. When using typed models with `createModel`, all store methods are fully typed. TypeScript catches type mismatches at compile time: ```tsx interface PageModel { count: number; name: string; } const m = createModel(); store.set(m.count, 5); // ✓ OK store.set(m.count, "five"); // ✗ Type error store.set(m.name, "John"); // ✓ OK ``` ## Store Methods The store provides several methods for working with data: | Method | Description | | ------------------------------ | ------------------------------------------------------------------ | | `get(accessor)` | Returns the value at the given path | | `set(accessor, value)` | Sets the value at the given path | | `init(accessor, value)` | Sets the value only if currently undefined | | `update(accessor, fn)` | Applies a function to the current value and stores the result | | `delete(accessor)` | Removes the value at the given path | | `toggle(accessor)` | Inverts a boolean value | | `copy(from, to)` | Copies a value from one path to another | | `move(from, to)` | Moves a value from one path to another | | `batch(fn)` | Batches multiple updates, notifying listeners only once at the end | | `silently(fn)` | Executes updates without triggering any notifications | | `notify(path?)` | Manually triggers change notifications | | `subscribe(fn)` | Registers a listener for changes; returns an unsubscribe function | | `ref(accessor, defaultValue?)` | Creates a reactive reference to store data | | `getData()` | Returns the entire store data object | ```tsx import { createModel } from "cx/data"; import { Button } from "cx/widgets"; interface PageModel { user: { name: string; age: number; }; } const m = createModel(); export default (
Store content
 JSON.stringify(data, null, 2)} />
    
); ``` ## Immutability The store treats all data as immutable. When you call `set()` or `update()`, CxJS creates new object references along the path to the changed value. This enables efficient change detection — widgets only re-render when their bound data actually changes. When updating objects or arrays, you must create new instances. Mutating existing objects directly will not trigger UI updates. ```tsx // Updating an object - spread the original and override properties store.update(m.user, (user) => ({ ...user, name: "John" })); // Adding to an array - create a new array store.update(m.items, (items) => [...items, newItem]); // Removing from an array - filter returns a new array store.update(m.items, (items) => items.filter((item) => item.id !== id)); ``` ## Immer Integration For complex nested updates, manually spreading objects can become tedious. The `cx-immer` package provides a `mutate` method that lets you write mutable-style code while CxJS handles immutability behind the scenes: ```tsx import { enableImmerMutate } from "cx-immer"; enableImmerMutate(); // Now you can use mutate with mutable syntax store.mutate(m.user, (user) => { user.name = "John"; user.scores.push(100); user.address.city = "New York"; }); ``` The `mutate` method uses [Immer](https://immerjs.github.io/immer/) to produce immutable updates from mutable code. This is especially useful when updating deeply nested structures or performing multiple changes at once. ## Creating a Store In most applications, CxJS creates the store automatically. If you need to create one manually (for testing or advanced scenarios), use the `Store` constructor: ```tsx import { Store } from "cx/data"; const store = new Store({ data: { count: 0, user: { name: "Guest" }, }, }); // Read and write data const count = store.get(m.count); store.set(m.count, count + 1); ``` --- # Data Binding Data binding connects your UI to the store, enabling automatic synchronization between widgets and application state. When store data changes, bound widgets update automatically. When users interact with widgets, their changes flow back to the store. ## Accessor Chains The primary way to bind data in CxJS is through **accessor chains** created with `createModel`. Pass an accessor directly to widget properties for two-way binding: ```tsx import { createModel } from "cx/data"; import { TextField, Slider } from "cx/widgets"; interface PageModel { name: string; volume: number; } const m = createModel(); export default (
Store content
 JSON.stringify(data, null, 2)} />
    
); ``` When you assign `m.name` to the `value` property, CxJS creates a two-way binding. The TextField displays the current value and writes changes back to the store. ### Default Values with bind Use `bind` to provide a default value when the store path is undefined: ```tsx import { createModel } from "cx/data"; import { bind } from "cx/ui"; import { TextField, NumberField } from "cx/widgets"; interface PageModel { username: string; count: number; } const m = createModel(); export default (
Username:
Count:
Store content
 JSON.stringify(data, null, 2)} />
    
); ``` When the widget initializes, if the store path is undefined, the default value is automatically written to the store. ## Computed Values with expr Use `expr` to compute values from one or more store paths. The function recalculates whenever any of its dependencies change: ```tsx import { createModel } from "cx/data"; import { TextField, NumberField } from "cx/widgets"; import { expr } from "cx/ui"; interface PageModel { firstName: string; lastName: string; price: number; quantity: number; } const m = createModel(); export default (
Full name: `${first || ""} ${last || ""}`.trim(), )} />
Total: { let total = (price || 0) * (qty || 0); return `$${total.toFixed(2)}`; })} />
); ``` The `expr` function takes accessor chains as arguments, followed by a compute function that receives the current values: ```tsx expr(m.firstName, m.lastName, (first, last) => `${first} ${last}`); ``` ## Computed Values with computable For complex calculations, use `computable` instead of `expr`. It works the same way but adds **memoization** — the result is cached and only recalculated when dependencies actually change: ```tsx import { computable } from "cx/data"; // Memoized computation - result cached until items or taxRate changes const total = computable(m.items, m.taxRate, (items, taxRate) => { const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0); return subtotal * (1 + taxRate); }); ``` Use `computable` when the calculation is expensive or when the same value is used in multiple places. ## Formatting Values Use `format` to apply format strings to bound values: ```tsx import { format } from "cx/ui"; ``` Use `tpl` to combine multiple values into formatted text: ```tsx import { tpl } from "cx/ui"; // Positional placeholders

// With formatting

// With null fallback

``` See [Formatting](/docs/intro/formatting) for the complete format syntax reference. ## Expression Helpers CxJS provides type-safe helper functions for common boolean expressions. These return `Selector` and are useful for properties like `visible`, `disabled`, and `readOnly`: ```tsx import { truthy, isEmpty, greaterThan } from "cx/ui";

User has a name
No items available
User is an adult
``` | Helper | Description | | ------------------------------------- | ------------------------------------------------------ | | `truthy(accessor)` | Evaluates truthiness | | `falsy(accessor)` | Evaluates falsiness | | `isTrue(accessor)` | Strict `true` check | | `isFalse(accessor)` | Strict `false` check | | `hasValue(accessor)` | Checks for non-null/undefined | | `isEmpty(accessor)` | Checks for empty strings/arrays | | `isNonEmpty(accessor)` | Checks for non-empty strings/arrays | | `equal(accessor, value)` | Loose equality comparison | | `notEqual(accessor, value)` | Loose inequality comparison | | `strictEqual(accessor, value)` | Strict equality comparison | | `strictNotEqual(accessor, value)` | Strict inequality comparison | | `greaterThan(accessor, value)` | Numeric greater than | | `lessThan(accessor, value)` | Numeric less than | | `greaterThanOrEqual(accessor, value)` | Numeric greater than or equal | | `lessThanOrEqual(accessor, value)` | Numeric less than or equal | | `format(accessor, formatString)` | Formats value using [format strings](/core/formatting) | ## Legacy Binding Syntax The following binding methods are supported for backwards compatibility but are not recommended for new code. ### String-based bind Before typed models, bindings used string paths: ```tsx import { bind } from "cx/data"; // Legacy string-based binding // Modern accessor chain (preferred) ``` ### String-path templates The `tpl` function also supports string-path syntax: ```tsx import { tpl } from "cx/data"; // Legacy string-path template
// Modern typed accessor (preferred)
``` ### Attribute suffixes In older CxJS code, you may see attribute suffixes like `-bind`, `-expr`, and `-tpl`. These require Babel plugins and are not supported in the TypeScript-first approach: ```tsx // Legacy attribute suffixes (requires Babel plugin)
``` --- # Controllers Controllers contain the business logic for your views. They handle data initialization, event callbacks, computed values, and reactions to data changes. ## Creating a Controller Extend the `Controller` class and attach it to a widget using the `controller` property. The controller has access to the store and can define methods that widgets call: ```tsx import { createModel } from "cx/data"; import { Controller } from "cx/ui"; import { Button, TextField } from "cx/widgets"; interface PageModel { name: string; greeting: string; } const m = createModel(); class PageController extends Controller { onInit() { this.store.init(m.name, "World"); } greet() { let name = this.store.get(m.name); this.store.set(m.greeting, `Hello, ${name}!`); } clear() { this.store.delete(m.greeting); } } export default (
Store content
 JSON.stringify(data, null, 2)} />
    
); ``` The controller's methods are available to all widgets within its scope. In event handlers, access the controller through the second parameter. ## Inline Controllers For simple cases, define a controller inline using an object: ```tsx import { createModel } from "cx/data"; import { NumberField } from "cx/widgets"; import { tpl } from "cx/ui"; interface PageModel { count: number; double: number; } const m = createModel(); export default (
c * 2); }, }} class="flex items-center gap-4" >
); ``` The inline form supports lifecycle methods and controller features like `addTrigger` and `addComputable`. ## Lifecycle Methods Controllers have lifecycle methods that run at specific times: | Method | Description | | ------------- | -------------------------------------------------------------------------------- | | `onInit()` | Runs once when the controller is created. Use for data initialization and setup. | | `onExplore()` | Runs on every render cycle during the explore phase. | | `onDestroy()` | Runs when the controller is destroyed. Use for cleanup (timers, subscriptions). | ```tsx class PageController extends Controller { timer: number; onInit() { // Initialize data this.store.init(m.count, 0); // Start a timer this.timer = window.setInterval(() => { this.store.update(m.count, (c) => c + 1); }, 1000); } onDestroy() { // Clean up window.clearInterval(this.timer); } } ``` ## Typed Controller Access Use `getControllerByType` to get a typed reference to a controller. This provides full autocomplete and compile-time type checking: ```tsx import { createModel } from "cx/data"; import { Controller } from "cx/ui"; import { Button } from "cx/widgets"; interface PageModel { count: number; } const m = createModel(); class CounterController extends Controller { onInit() { this.store.init(m.count, 0); } increment(amount: number = 1) { this.store.update(m.count, (count) => count + amount); } decrement(amount: number = 1) { this.store.update(m.count, (count) => count - amount); } reset() { this.store.set(m.count, 0); } } export default (
); ``` The `getControllerByType` method searches up the widget tree and returns a typed controller instance. ## Triggers Triggers watch store paths and run callbacks when values change. Use `addTrigger` in `onInit`: ```tsx class PageController extends Controller { onInit() { this.addTrigger( "selection-changed", [m.selectedId], (selectedId) => { if (selectedId) { this.loadDetails(selectedId); } }, true, ); // true = run immediately } async loadDetails(id: string) { let data = await fetch(`/api/items/${id}`).then((r) => r.json()); this.store.set(m.details, data); } } ``` The trigger name allows you to remove it later with `removeTrigger("selection-changed")`. ## Computables Add computed values that automatically update when dependencies change: ```tsx class PageController extends Controller { onInit() { this.addComputable(m.fullName, [m.firstName, m.lastName], (first, last) => { return `${first || ""} ${last || ""}`.trim(); }); this.addComputable(m.total, [m.items], (items) => { return items?.reduce((sum, item) => sum + item.price, 0) || 0; }); } } ``` The first argument is the store path where the result is written. The computed value updates whenever any dependency changes. ## Accessing Parent Controllers Use `getParentControllerByType` to get a typed reference to a parent controller: ```tsx class ChildController extends Controller { onSave() { let parent = this.getParentControllerByType(PageController); parent.saveChild(this.getData()); } } ``` This provides full type safety and autocomplete. For dynamic method invocation by name, use `invokeParentMethod`: ```tsx this.invokeParentMethod("onSave", this.getData()); ``` --- # Formatting CxJS provides built-in support for formatting numbers, dates, and currencies. Formats can be applied to widgets via the `format` property or programmatically using `Format.value()`. ## Using Formats Widgets like `NumberField` and `DateField` accept a `format` property that controls how values are displayed: ```tsx import { createModel } from "cx/data"; import { Format } from "cx/util"; import { bind, expr, format, LabelsTopLayout } from "cx/ui"; import { NumberField, DateField } from "cx/widgets"; interface PageModel { price: number; quantity: number; date: Date; } const m = createModel(); export default (
Format Result
currency;USD;2
n;0
d;yyMd
d;DDDDyyyyMMMMd Format.value(date, "d;DDDDyyyyMMMMd"))} />
); ``` For formatting bound values in text, see the [Formatting Values](/docs/intro/data-binding#formatting-values) section in Data Binding. ## Culture-Sensitive Formatting Date, currency, and number formats depend on culture settings. Enable culture-sensitive formatting before use: ```tsx import { enableCultureSensitiveFormatting } from "cx/ui"; enableCultureSensitiveFormatting(); ``` This ensures formats respect locale-specific conventions for decimal separators, date order, currency symbols, and month/day names. ## Format Syntax Format strings use semicolons to separate parameters: ``` formatType;param1;param2 ``` Use a pipe `|` to specify text for null values: ``` n;2|N/A ``` Chain multiple formats with colons (applied left-to-right): ``` n;2:suffix; USD ``` ## Number Formats Number formats support min and max decimal places, plus optional flags: `n;minDecimals;maxDecimals;flags`. If only one decimal value is provided, it's used for both min and max. | Format | Description | Example Input | Example Output | | --------- | --------------------------- | ------------- | -------------- | | `n` | Number | `1234.5` | `1,234.5` | | `n;0` | No decimals | `1234.5` | `1,235` | | `n;2` | Exactly 2 decimals | `1234.5` | `1,234.50` | | `n;0;2` | 0-2 decimals | `1234.5` | `1,234.5` | | `n;0;0;+` | Plus sign for positive | `1234` | `+1,234` | | `n;0;0;a` | Accounting format | `-1234` | `(1,234)` | | `n;0;0;c` | Compact notation | `105000` | `105K` | | `p` | Percentage (×100) | `0.25` | `25%` | | `p;0;2` | Percentage, 0-2 decimals | `0.256` | `25.6%` | | `ps;0;2` | Percent sign only (no ×100) | `25.6` | `25.6%` | ## Currency Format The `currency` format supports an optional currency code, decimal places, and flags: `currency;code;minDecimals;maxDecimals;flags`. The currency code can be omitted to use the default currency. | Format | Description | Example Input | Example Output | | -------------------- | ---------------------------- | ------------- | -------------- | | `currency` | Default currency | `1234.5` | `$1,234.50` | | `currency;USD` | US Dollars | `1234.5` | `$1,234.50` | | `currency;EUR` | Euros | `1234.5` | `€1,234.50` | | `currency;USD;0` | USD, no decimals | `1234.5` | `$1,235` | | `currency;;2` | Default currency, 2 decimals | `1234.5` | `$1,234.50` | | `currency;USD;2;2;+` | Plus sign for positive | `1234.5` | `+$1,234.50` | | `currency;USD;2;2;a` | Accounting format | `-1234.5` | `($1,234.50)` | | `currency;;0;0;c` | Compact notation | `105000` | `$105K` | ## Format Flags These flags can be added to number and currency formats: | Flag | Description | | ---- | -------------------------------------------------- | | `+` | Display plus sign for positive numbers | | `a` | Accounting format (negative values in parentheses) | | `c` | Compact notation (e.g., 105K, 1M) | Flags can be combined, e.g., `+ac` for all three options. The default currency is determined by culture settings. ## String Formats String formats allow adding prefixes, suffixes, and wrapping values: | Format | Description | Example Input | Example Output | | ------------ | -------------------- | ------------- | -------------- | | `prefix;Hi ` | Add prefix | `"John"` | `"Hi John"` | | `suffix; cm` | Add suffix | `180` | `"180 cm"` | | `wrap;(;)` | Wrap with delimiters | `5` | `"(5)"` | These can be chained with other formats: ```tsx Format.value(5, "n;2:wrap;(;)"); // "(5.00)" Format.value(180, "n;0:suffix; cm"); // "180 cm" ``` ## Date Formats Date formats use pattern codes concatenated together. Separators are provided by the culture settings, not the pattern. | Format | Description | Example Output | | ----------------- | ------------------------- | ---------------------------- | | `d` | Default date | `2/1/2024` | | `d;yyMd` | Short year | `2/1/24` | | `d;yyMMdd` | 2-digit year, padded | `02/01/24` | | `d;yyyyMMdd` | Full date, padded | `02/01/2024` | | `d;yyyyMMMd` | Abbreviated month | `Feb 1, 2024` | | `d;yyyyMMMMdd` | Full month name | `February 01, 2024` | | `d;DDDyyyyMd` | Short weekday | `Thu, 2/1/2024` | | `d;DDDDyyyyMMMdd` | Full weekday, short month | `Thursday, Feb 01, 2024` | | `d;DDDDyyyyMMMMd` | Full weekday and month | `Thursday, February 1, 2024` | ### Date Pattern Characters Pattern codes are concatenated without separators. Four characters means full name, three is abbreviated, two is padded, one is numeric. | Character | Description | | --------- | ------------------- | | `yyyy` | 4-digit year | | `yy` | 2-digit year | | `MMMM` | Full month name | | `MMM` | Abbreviated month | | `MM` | 2-digit month | | `M` | Month number | | `dd` | 2-digit day | | `d` | Day number | | `DDDD` | Full weekday name | | `DDD` | Abbreviated weekday | ### Time Pattern Characters | Character | Description | | --------- | ------------------- | | `HH` | Hours (padded) | | `H` | Hours | | `mm` | Minutes (padded) | | `m` | Minutes | | `ss` | Seconds (padded) | | `s` | Seconds | | `a` / `A` | AM/PM indicator | | `N` | 24-hour format flag | Date and time patterns can be combined: `d;yyyyMMddHHmm` formats both date and time. Add `N` for 24-hour format: `d;yyyyMMddNHHmm`. For more details on culture-sensitive formatting, see [intl-io](https://github.com/codaxy/intl-io). ## Programmatic Formatting Use `Format.value()` to format values in code: ```tsx import { Format } from "cx/util"; Format.value(1234.5, "n;2"); // "1,234.50" Format.value(0.15, "p;0"); // "15%" Format.value(new Date(), "d;yyyyMMdd"); // "02/01/2024" ``` Use the `format` helper to format bound values: ```tsx import { format } from "cx/ui"; ; ``` Alternatively, use `expr` with `Format.value` for more control: ```tsx import { expr } from "cx/ui"; import { Format } from "cx/util"; Format.value(price, "currency;USD"))} />; ``` ## String Templates Use `StringTemplate.format` when you need to combine multiple values into a single formatted string: ```tsx import { StringTemplate } from "cx/util"; // Positional arguments StringTemplate.format( "{0} bought {1} items for {2:currency;USD}", "John", 5, 49.99, ); // "John bought 5 items for $49.99" // Named properties with an object StringTemplate.format("{name} - {date:d;yyyyMMdd}", { name: "Report", date: new Date(), }); // "Report - 02/01/2024" ``` Use `StringTemplate.compile` to create a reusable formatter function: ```tsx const formatter = StringTemplate.compile("{name}: {value:currency;USD}"); formatter({ name: "Total", value: 99.99 }); // "Total: $99.99" formatter({ name: "Tax", value: 7.5 }); // "Tax: $7.50" ``` ## Custom Formats Register custom formats using `Format.register`: ```tsx import { Format } from "cx/util"; // Simple format Format.register("brackets", (value) => `(${value})`); // Use it Format.value("test", "brackets"); // "(test)" ``` For formats with parameters, use `Format.registerFactory`: ```tsx Format.registerFactory("suffix", (format, suffix) => { return (value) => value + suffix; }); // Use it Format.value(100, "suffix; kg"); // "100 kg" ``` --- # HtmlElement The `HtmlElement` widget renders HTML elements with CxJS data binding support. The CxJS JSX runtime automatically converts all lowercase elements (like `div`, `span`, `p`) to `HtmlElement` instances with the corresponding `tag` property set. You can also use `HtmlElement` directly when you need to specify the tag dynamically or prefer explicit syntax. HTML elements can be freely mixed with CxJS widgets like `TextField`, allowing you to build forms and layouts that combine standard HTML with rich interactive components. ```tsx import { createModel } from "cx/data"; import { tpl } from "cx/ui"; import { HtmlElement, TextField } from "cx/widgets"; interface Model { name: string; } const m = createModel(); export const model = { name: "World", }; export default (

Heading

Paragraph with some text.

Using HtmlElement directly

); ``` ## Key Features - Lowercase JSX elements are automatically converted to `HtmlElement` by the JSX runtime - All standard HTML attributes and events work as expected - CxJS-specific attributes like `visible`, `layout`, `controller` are supported - Use `text` prop with `tpl()` for data-bound text content - Mix freely with CxJS widgets ## Configuration | Property | Type | Description | | -------- | ---- | ----------- | | `tag` | `string` | Name of the HTML element to render. Default is `div`. | | `text` / `innerText` | `string` | Inner text contents. | | `innerHtml` / `html` | `string` | HTML to be injected into the element. | | `tooltip` | `string \| object` | Tooltip configuration. | | `autoFocus` | `boolean` | Set to `true` to automatically focus the element when mounted. | | `baseClass` | `string` | Base CSS class to be applied to the element. | --- # PureContainer `PureContainer` groups multiple widgets together without adding any HTML markup to the DOM. This is useful when you need to control visibility or apply a layout to a group of elements. ```tsx import { createModel } from "cx/data"; import { LabelsTopLayout, PureContainer } from "cx/ui"; import { Checkbox, TextField } from "cx/widgets"; interface Model { showContactInfo: boolean; email: string; phone: string; } const m = createModel(); export const model = { showContactInfo: true, email: "", phone: "", }; export default (
Show contact information
Contact Information

We'll never share your contact information.

); ``` ## Common Use Cases - Toggle visibility of multiple widgets at once using `visible` - Apply a shared `layout` to a group of form fields - Base class for other CxJS components like `ValidationGroup`, `Repeater`, and `Route` ## Configuration | Property | Type | Description | | -------- | ---- | ----------- | | `visible` | `boolean` | Controls visibility of all children. | | `layout` | `string \| object` | Inner layout applied to children. | | `items` / `children` | `array` | List of child elements. | | `controller` | `object` | Controller instance for this container. | | `trimWhitespace` | `boolean` | Remove whitespace in text children. Default is `true`. | | `preserveWhitespace` / `ws` | `boolean` | Keep whitespace in text children. Default is `false`. | --- # ContentResolver `ContentResolver` dynamically resolves content at runtime based on data. Use it when the content to display is unknown at build time, depends on data values, or needs to be lazy loaded. ```tsx import { createModel } from "cx/data"; import { Checkbox, ContentResolver, DateField, LookupField, Switch, TextField, } from "cx/widgets"; interface Model { fieldType: string; text: string; date: string; checked: boolean; } const m = createModel(); const fieldTypes = [ { id: "textfield", text: "TextField" }, { id: "datefield", text: "DateField" }, { id: "checkbox", text: "Checkbox" }, { id: "switch", text: "Switch" }, ]; export default (
{ switch (type) { case "textfield": return ; case "datefield": return ; case "checkbox": return Checked; case "switch": return ; default: return null; } }} />
); ``` ## How It Works 1. The `params` prop binds to a value in the store 2. When `params` changes, `onResolve` is called with the new value 3. `onResolve` returns the widget configuration to render 4. For async loading, `onResolve` can return a Promise 5. Children are displayed as default content while loading ## Structured Params The `params` prop can be a structured object with multiple bindings: ```jsx { // resolve based on multiple parameters }} /> ``` When any of the bound values change, `onResolve` is called with the updated object. ## Configuration | Property | Type | Description | | -------- | ---- | ----------- | | `params` | `any` | Parameter binding. Can be a single value or structured object. Content is recreated when params change. | | `onResolve` | `function` | Callback taking `params` and returning widget configuration or a Promise. | | `mode` | `string` | How resolved content combines with children: `replace`, `prepend`, or `append`. Default is `replace`. | | `loading` | `boolean` | Writable binding set to `true` while a Promise is resolving. | | `children` | `any` | Default content displayed while `onResolve` Promise is loading. | --- # Functional Components Functional components provide a simple way to create reusable structures composed of CxJS widgets. Use `createFunctionalComponent` to define a CxJS functional component: ```tsx import { createModel } from "cx/data"; import type { AccessorChain } from "cx/ui"; import { bind, createFunctionalComponent } from "cx/ui"; import { Button } from "cx/widgets"; interface PageModel { count1: number; count2: number; } const m = createModel(); interface CounterProps { value: AccessorChain; label: string; } const Counter = createFunctionalComponent(({ value, label }: CounterProps) => (
{label}:
)); export default (
); ``` The `Counter` component can be reused with different props, each instance maintaining its own state through different store bindings. ## Example 2 Functional components are useful for creating reusable chart configurations: ```tsx import { createFunctionalComponent } from "cx/ui"; import { Svg } from "cx/svg"; import { Chart, Gridlines, LineGraph, NumericAxis } from "cx/charts"; interface LineChartProps { data: { x: number; y: number }[]; chartStyle?: string; lineStyle?: string; areaStyle?: string; } const LineChart = createFunctionalComponent( ({ data, chartStyle, lineStyle, areaStyle }: LineChartProps) => ( ), ); export default (
({ x, y: 75 - 50 * Math.random(), }))} /> ({ x, y: 75 - 50 * Math.random(), }))} /> ({ x, y: 75 - 50 * Math.random(), }))} />
); ``` The same `LineChart` component renders three different chart styles by passing different props. ## Conditional Logic Functional components can contain conditional logic: ```tsx import { createModel } from "cx/data"; import { createFunctionalComponent, LabelsLeftLayout, LabelsTopLayout, } from "cx/ui"; import { TextField } from "cx/widgets"; interface PageModel { form: { firstName: string; lastName: string; }; } const m = createModel(); interface MyFormProps { vertical?: boolean; } const MyForm = createFunctionalComponent(({ vertical }: MyFormProps) => { let layout = !vertical ? LabelsLeftLayout : { type: LabelsTopLayout, vertical: true }; return (
); }); export default (
); ``` The `vertical` prop changes the layout at render time. ## Reserved Properties These properties are handled by the framework and should not be used inside the function body: | Property | Type | Description | | ------------- | ------------ | ---------------------------------------------------------------------------------------- | | `visible` | `boolean` | If `false`, the component won't render and its controller won't initialize. Alias: `if`. | | `controller` | `Controller` | Controller that will be initialized with the component. | | `layout` | `Layout` | Inner layout applied to child elements. | | `outerLayout` | `Layout` | Outer layout that wraps the component. | | `putInto` | `string` | Content placeholder name for outer layouts. Alias: `contentFor`. | --- # Data View Components All application data in CxJS is stored inside a central [Store](/docs/intro/store). While convenient for global state, accessing deeply nested paths or working with collections can become cumbersome. Data View components wrap parts of the widget tree and provide a modified view of the Store data, making it easier to work with specific areas of the data model. ## Comparison | Component | Purpose | Use case | | -------------------------------------- | ------------------------------------------------- | ------------------------------------- | | [Repeater](./repeater) | Renders children for each record in a collection | Lists, tables, any repeated content | | [Rescope](./rescope) | Selects a common prefix for shorter binding paths | Deeply nested data structures | | [Sandbox](./sandbox) | Multiplexes data based on a dynamic key | Tabs, routes with isolated page data | | [PrivateStore](./private-store) | Creates an isolated store for a subtree | Reusable components with local state | | [DataProxy](./data-proxy) | Creates aliases with custom getter/setter logic | Computed values, data transformations | | [Route](./route) | Renders children when URL matches a pattern | Page routing, exposes `$route` params | ## How Data Views Work Each Data View component exposes the same interface as the Store to its children, but can introduce additional properties. For example, Repeater adds `$record` and `$index` for each item in the collection, Route exposes `$route` with matched URL parameters, while Sandbox might expose `$page` for route-specific data. These additional properties are only accessible within the scope of that Data View, allowing child widgets to bind to them just like any other Store data. ## How to Choose Use [Repeater](./repeater) when you need to render a list of items from an array. Use [Rescope](./rescope) when working with deeply nested data and you want shorter binding paths. Use [Sandbox](./sandbox) when you need to switch between different data contexts based on a key (e.g., tabs, route parameters). Use [PrivateStore](./private-store) (also known as Restate) when you need completely isolated state that doesn't affect the global store. Use [DataProxy](./data-proxy) when you need to transform data or create computed aliases with custom getter/setter logic. Use [Route](./route) when you need to conditionally render content based on URL and access matched route parameters. ## Store Mutation By default, Data View components write aliased data (like `$record`, `$page`) back to the parent store, all the way up to the global store. Regular data bindings propagate normally regardless of these settings. This default behavior is often fine and can improve performance by avoiding data copying. However, sometimes you want to prevent aliased fields from polluting your data. For example, when rendering a tree with nested Repeaters, fields like `$record` and `$index` would be written into your tree nodes. Two properties control this behavior: - `immutable` - Prevents aliased data from being written to the parent store. - `sealed` - Prevents child Data Views from writing aliased data to this Data View's store. --- # Repeater Repeater renders its children for each record in a collection. Use `recordAlias` to specify an accessor for accessing record data within the repeater. ## Example ```tsx import { createModel } from "cx/data"; import { Controller, expr } from "cx/ui"; import { Button, Checkbox, Repeater } from "cx/widgets"; interface Item { text: string; checked: boolean; } interface PageModel { items: Item[]; $record: Item; } const m = createModel(); class PageController extends Controller { onInit() { this.reset(); } reset() { this.store.set(m.items, [ { text: "Learn CxJS basics", checked: true }, { text: "Build a sample app", checked: false }, { text: "Master data binding", checked: false }, ]); } } export default (
Completed:{" "} items.filter((a) => a.checked).length, )} />{" "} of tasks
); ``` ## Typed Model Add `$record` to your model interface to get type-safe access to record data: ```tsx interface PageModel { items: Item[]; $record: Item; } const m = createModel(); ``` Then use `recordAlias={m.$record}` to bind the record accessor: ```tsx ``` ## Accessing the Record Store Event handlers like `onClick` receive an instance object as the second argument. This instance contains a `store` that provides access to the record-specific data view. Use this to manipulate individual records: ```tsx

Home

Welcome to the home page.

About

Learn more about us.

Contact

Get in touch with us.

Current URL:
); ``` ## Route Patterns Routes support parameters, splats, and optional parts using [route-parser](https://github.com/rcs/route-parser#what-can-i-use-in-my-routes) syntax: | Pattern | Description | Example Match | | --------------- | -------------------- | -------------------------------- | | `~/users` | Exact match | `/users` | | `~/users/:id` | Named parameter | `/users/123` → `id: "123"` | | `~/files/*path` | Splat (rest of path) | `/files/a/b/c` → `path: "a/b/c"` | | `~/users(/:id)` | Optional parameter | `/users` or `/users/123` | ## Prefix Matching Use `prefix` to match routes that start with a pattern: ```tsx {/* Matches ~/admin, ~/admin/users, ~/admin/settings, etc. */} ``` ## Nested Routes Use `+/` to define routes relative to the parent: ```tsx ``` ## Configuration | Property | Type | Description | | ---------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `route` / `path` | `string` | Target route, e.g. `~/user/:userId`. Use `~/` to denote the application root path and `+/` in nested routes to append to the parent route. | | `url` | `Prop` | Url binding. Bind this to the global `url` variable. | | `prefix` | `boolean` | Match route even if given `route` is only a prefix of the current `url`. Used when a route contains nested subroutes. | | `params` | `Prop` | Params binding. Matched route parameters will be stored inside. | --- # Url The Url helper class provides methods for working with URL paths, particularly for resolving the `~/` prefix used in routes. ## Setting the Base Path Before using routes, set the application's base path. This is required if your app is hosted in a subdirectory: ```tsx // Set explicitly Url.setBase("/my-app/"); // Or detect from a script tag Url.setBaseFromScript("~/app.js"); ``` If not set, the default base path is `/`. ## Methods | Method | Description | | ----------------------------------- | ----------------------------------------------------------- | | `Url.setBase(base)` | Sets the base path of the application | | `Url.setBaseFromScript(scriptPath)` | Sets base path by finding a matching script `src` attribute | | `Url.resolve(path)` | Resolves `~/` to the application base path | | `Url.unresolve(path)` | Converts an absolute path back to `~/` format | | `Url.isLocal(path)` | Checks if a path is within the application | ## Examples ```tsx Url.setBase("/docs/"); Url.resolve("~/page"); // "/docs/page" Url.unresolve("/docs/page"); // "~/page" Url.isLocal("/docs/"); // true Url.isLocal("/other/"); // false ``` --- # History The History class handles HTML5 `pushState` navigation, connecting the browser URL to your store. ## Setup Connect History to your store at application startup: ```tsx History.connect(store, "url"); ``` The store is updated whenever navigation happens, allowing the application to re-render and display relevant content. ## Methods | Method | Description | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `History.connect(store, bind)` | Initializes a link between browser's location and store variable pointed by the `bind` argument | | `History.pushState(state, title, url)` | Performs navigation to a new location without refreshing the page | | `History.replaceState(state, title, url)` | Performs navigation to a new location. Current location will not be saved in browser's history. | | `History.subscribe(callback)` | Subscribe to location changes. Useful for setting up page tracking (e.g. Google Analytics). Returns an unsubscribe function. | | `History.reloadOnNextChange()` | Instructs the router to reload the page on next navigation. Commonly used with service workers. | | `History.addNavigateConfirmation(callback, permanent)` | Instructs the router to execute the given callback to confirm leaving the current page. The callback is executed only for the current page, unless `permanent` is set to `true`. | ## Programmatic Navigation ```tsx // Add to history (back button will return here) History.pushState({}, null, "~/dashboard"); // Replace current entry (back button skips this) History.replaceState({}, null, "~/dashboard"); ``` ## Subscribing to Navigation Track page views or perform actions on navigation: ```tsx History.subscribe((url) => { analytics.trackPageView(url); }); ``` ## Navigation Confirmation Prompt users before they leave a page with unsaved changes: ```tsx History.addNavigateConfirmation((url) => { return MsgBox.yesNo("You have unsaved changes. Leave anyway?").then( (answer) => answer === "yes", ); }); ``` The callback receives the target URL and should return a boolean or `Promise`. By default, the confirmation is removed when leaving the page. Pass `true` as the second argument to make it permanent. ## Browser Support Browsers without `pushState` support fall back to standard navigation (full page reload). --- # Drag and Drop CxJS provides components for implementing drag and drop functionality: - [DragSource](/docs/concepts/drag-source) - Wraps elements that can be dragged - [DropZone](/docs/concepts/drop-zone) - Defines areas where items can be dropped - [DragHandle](/docs/concepts/drag-handle) - Optional handle to initiate dragging ## Example This example demonstrates list reordering using drag and drop: ```tsx import { createModel } from "cx/data"; import { Controller } from "cx/ui"; import { DragSource, DropZone, Repeater } from "cx/widgets"; interface Item { id: number; text: string; } interface PageModel { items: Item[]; $record: Item; $index: number; } const m = createModel(); class PageController extends Controller { onInit() { this.store.set(m.items, [ { id: 1, text: "Apple" }, { id: 2, text: "Banana" }, { id: 3, text: "Cherry" }, { id: 4, text: "Date" }, { id: 5, text: "Elderberry" }, ]); } } export default (
{ let targetIndex = store.get(m.$index); let items = store .get(m.items) .filter((item) => item.id !== source.data.id); store.set(m.items, [ ...items.slice(0, targetIndex), source.data, ...items.slice(targetIndex), ]); }} matchWidth matchHeight matchMargin inflate={50} /> { let items = store .get(m.items) .filter((item) => item.id !== source.data.id); store.set(m.items, [...items, source.data]); }} inflate={50} >
); ``` ## How It Works 1. Each item is wrapped in a `DragSource` that passes the record data when dragged 2. A `DropZone` around each item receives the dropped data 3. The `onDrop` handler reorders the array by removing the item from its old position and inserting it at the new position --- # Button Buttons trigger actions when clicked. They support various visual styles, icons, and built-in confirmation dialogs. ```tsx import { Button } from "cx/widgets"; export default (
); ``` Use `pressed` to show a toggled state, and `disabled` to prevent interaction. ## Mods Buttons support visual modifiers: `primary` for main actions, `danger` for destructive actions, and `hollow` for secondary actions. ```tsx import { Button } from "cx/widgets"; export default (
); ``` ## Icons Add icons using the `icon` prop. Buttons can have both icon and text, or just an icon. ```tsx import { Button } from "cx/widgets"; import "../../icons/lucide"; export default (
); ``` ## Confirmation Use the `confirm` prop to show a confirmation dialog before executing the `onClick` handler. ```tsx import { Button, MsgBox } from "cx/widgets"; export default (
); ``` To enable CxJS-based confirmation dialogs, call `enableMsgBoxAlerts()` at application startup: ```js import { enableMsgBoxAlerts } from "cx/widgets"; enableMsgBoxAlerts(); ``` ## Configuration | Property | Type | Description | | ------------------ | ------------------ | ----------------------------------------------------- | | `text` | `StringProp` | Button text content. | | `icon` | `StringProp` | Name of the icon to display. | | `mod` | `string` | Visual modifier: `primary`, `danger`, `hollow`. | | `pressed` | `BooleanProp` | Shows the button in a pressed/toggled state. | | `disabled` | `BooleanProp` | Disables the button. | | `enabled` | `BooleanProp` | Inverse of disabled. | | `confirm` | `string \| object` | Confirmation message or MsgBox configuration. | | `dismiss` | `boolean` | If true, closes the parent overlay when clicked. | | `submit` | `boolean` | Sets `type="submit"` for form submission. | | `type` | `string` | Button type: `submit` or `button`. Default: `button`. | | `focusOnMouseDown` | `boolean` | Allow focus on mouse click. Default: `false`. | | `onClick` | `function` | Click handler: `(e, instance) => void`. | --- # Tabs Tabs are used to switch between different views or content sections. Each `Tab` component writes its `tab` value to the shared `value` binding when clicked. ```tsx import { createModel } from "cx/data"; import { Tab } from "cx/widgets"; interface PageModel { tab: string; } const m = createModel(); export default (
Tab 1 Tab 2 Disabled
Tab 1 Tab 2 Disabled
Tab 1 Tab 2 Disabled
); ``` ## Tab Content Use the `visible` property with the `equal` helper to show content based on the selected tab: ```tsx import { createModel } from "cx/data"; import { equal } from "cx/ui"; import { Tab } from "cx/widgets"; interface PageModel { tab: string; } const m = createModel(); export default (
Profile Settings Notifications
Profile content goes here.
Settings content goes here.
Notifications content goes here.
); ``` ## Configuration | Property | Type | Description | | -------- | ---- | ----------- | | `tab` | `string \| number` | Value to write to `value` when the tab is clicked. | | `value` | `string \| number` | Binding to the currently selected tab. Tab appears active when `value` equals `tab`. | | `mod` | `string` | Visual modifier. Common values: `line`, `classic`. | | `default` | `boolean` | Set to `true` to make this the default tab. | | `disabled` | `boolean` | Set to `true` to disable the tab. | | `focusOnMouseDown` | `boolean` | If `true`, tab receives focus on mouse click. Default is `false`. | | `baseClass` | `string` | Base CSS class. Default is `tab`. | | `className` | `string` | Additional CSS class to apply. | | `style` | `string \| object` | Additional styles to apply. | --- # Text The `Text` component renders dynamic text content with data binding support. Use it when you need to display text values from the store. ```tsx import { createModel } from "cx/data"; import { Text, TextField } from "cx/widgets"; import { LabelsLeftLayout } from "cx/ui"; interface PageModel { name: string; } const m = createModel(); export default ( ); ``` ## Configuration | Property | Type | Description | | -------- | ---- | ----------- | | `value` | `string` | Text value to display. Supports data binding. | | `bind` | `string` | Store path containing the text value. Equivalent to `value={bind("path")}`. | | `tpl` | `string` | Template string. Equivalent to `value={tpl("template")}`. | | `expr` | `string` | Expression string. Equivalent to `value={expr("expression")}`. | --- # Link Links are used for `pushState` navigation between pages. The `Link` component renders an anchor element that integrates with CxJS routing. ```tsx import { Link } from "cx/widgets"; export default (
What is CxJS See LinkButton Disabled Link
); ``` ## Active State Links can automatically detect when they point to the current page and apply additional styling. Use `activeClass` or `activeStyle` to highlight active links in navigation menus. The `match` property controls how the URL is compared: - `equal` (default) - Link is active only when URLs match exactly - `prefix` - Link is active when `href` is a prefix of the current URL - `subroute` - Like `prefix`, but requires a `/` after the match (indicating a subroute) ## Configuration | Property | Type | Description | | -------- | ---- | ----------- | | `href` | `string` | URL to the link's target location. Use `~/` or `#/` prefix for pushState/hash based navigation. Use `+/` prefix for URLs relative to the parent route. | | `url` | `string` | Binding to the current URL location in the store. If `href` matches `url`, the `active` CSS class is applied. | | `match` | `string` | Determines how `href` is matched against `url` to detect active state. Supported values are `equal` (default), `prefix`, and `subroute`. | | `disabled` | `boolean` | Set to `true` to disable the link. | | `target` | `string` | Specifies where to open the linked document (e.g., `_blank`). | | `activeClass` | `string \| object` | Additional CSS class applied when the link is active. | | `activeStyle` | `string \| object` | Additional CSS style applied when the link is active. | | `inactiveClass` | `string \| object` | Additional CSS class applied when the link is not active. | | `inactiveStyle` | `string \| object` | Additional CSS style applied when the link is not active. | | `baseClass` | `string` | Base CSS class. Default is `link`. | --- # Icon The `Icon` component is used to render icons. CxJS includes only a few built-in icons; additional icon sets need to be registered. ```tsx import { Icon } from "cx/widgets"; import "../../icons/lucide"; export default (
); ``` ## Preregistered Icons CxJS includes the following built-in icons: `calendar`, `check`, `clear`, `close`, `cx`, `drop-down`, `file`, `folder`, `folder-open`, `forward`, `loading`, `menu`, `pixel-picker`, `search`, `sort-asc`, and `square`. To unregister icons (e.g., to replace them with custom versions): ```tsx Icon.unregister("search", "calendar"); ``` ## Registering Icons Individual icons can be registered using `Icon.register`: ```tsx Icon.register("custom-icon", ({ key, ...props }) => ( ... )); ``` ### FontAwesome FontAwesome icons can be registered using `Icon.registerFactory`: ```tsx Icon.registerFactory((name, { key, className, ...props }) => { return ( ); }); ``` ### Lucide Lucide icons need to be registered individually: ```tsx import { VDOM } from "cx/ui"; import { Icon } from "cx/widgets"; import type { IconNode } from "lucide"; import { Search, Plus, Pencil } from "lucide"; function getRenderer(iconNode: IconNode) { return ({ key, ...rest }: Record) => ( {iconNode.map(([tag, attrs], i) => VDOM.createElement(tag, { key: i, ...attrs }), )} ); } Icon.register("search", getRenderer(Search)); Icon.register("plus", getRenderer(Plus)); Icon.register("pencil", getRenderer(Pencil)); ``` ## Configuration | Property | Type | Description | | ----------- | ------------------ | ------------------------------------------------------------- | | `name` | `string` | Name under which the icon is registered. | | `key` | `string` | Unique key for the icon element. Passed to the icon renderer. | | `baseClass` | `string` | Base CSS class. Default is `icon`. | | `className` | `string` | Additional CSS class to apply to the icon. | | `style` | `string \| object` | Additional styles to apply to the icon. | --- # Outer Layouts Outer layouts wrap content in a reusable frame — a header, sidebar, footer, or any combination. Define the frame once, then reuse it across your application. ## Example ```tsx import { ContentPlaceholder, Content } from "cx/ui"; const AppLayout = (
App Header
); export default (

Welcome

This is the main content area.

); ``` ## How It Works 1. **Define the layout** with [ContentPlaceholder](/layout/content-placeholder) components marking where content goes 2. **Apply the layout** to any element using the `outerLayout` attribute 3. **Fill the placeholders** using [Content](/layout/content) or `putInto` ## Nested Layouts Layouts can contain other layouts. The content renders inside-out, allowing you to compose complex page structures from simple, reusable pieces. --- # ContentPlaceholder ```tsx import { ContentPlaceholder } from "cx/ui"; ``` ContentPlaceholder marks insertion points inside [outer layouts](/layout/outer-layouts). Content from child components flows into these placeholders. ## Usage ```tsx const AppLayout = (
); ``` The default placeholder (without a name) receives the main content. Named placeholders receive content targeted with [Content](/layout/content) or the `putInto` attribute. ## Configuration | Property | Type | Default | Description | | -------- | -------- | -------- | --------------------------------------------------- | | `name` | `string` | `"body"` | Placeholder identifier. Omit for main content area. | --- # Content ```tsx import { Content } from "cx/ui"; ``` Content sends child elements to a named [ContentPlaceholder](/layout/content-placeholder) inside an [outer layout](/layout/outer-layouts). ## Usage ```tsx
Main content here
``` ## Alternative: putInto Instead of wrapping content in a Content component, you can use the `putInto` or `contentFor` attribute on any element: ```tsx
Main content here
``` ## Configuration | Property | Type | Description | | -------------- | -------- | ----------------------- | | `for` / `name` | `string` | Target placeholder name | --- # Inner Layouts Inner layouts define how a widget's children are arranged. They are typically used as container components. | Layout | Purpose | | ------ | ------- | | [LabelsLeftLayout](labels-left-layout) | Horizontal form layout with labels on the left | | [LabelsTopLayout](labels-top-layout) | Form layout with labels above inputs | | [FirstVisibleChildLayout](first-visible-child-layout) | Shows only the first visible child | | [UseParentLayout](use-parent-layout) | Delegates layout to the parent container | ## Default Behavior (No Layout) When no layout is specified, children render in the order they are defined. For form fields, this means the label appears inline before the input: ```tsx import { createModel } from "cx/data"; import { TextField, Checkbox } from "cx/widgets"; interface FormModel { text: string; check: boolean; } const m = createModel(); export default (
First some text. Checkbox
); ``` This default behavior doesn't work well for structured forms. Use a layout component to arrange form fields properly: ```tsx import { createModel } from "cx/data"; import { LabelsLeftLayout } from "cx/ui"; import { TextField, Checkbox } from "cx/widgets"; interface FormModel { text: string; check: boolean; } const m = createModel(); export default ( First some text. Checkbox ); ``` --- # LabelsLeftLayout `LabelsLeftLayout` is used for horizontal form layouts. It renders children inside a table where labels go into the first column and inputs go into the second column. ```tsx import { createModel } from "cx/data"; import { LabelsLeftLayout } from "cx/ui"; import { TextField, NumberField, TextArea } from "cx/widgets"; interface FormModel { name: string; email: string; age: number; bio: string; } const m = createModel(); export default (