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 simple form with a text field and a button:
```tsx
import { createModel } from "cx/ui";
import { Button, LabelsTopLayout, MsgBox, TextField } from "cx/widgets";
interface PageModel {
name: string;
}
const m = createModel();
export default (
);
```
Let's break down the key parts:
- **PageModel** — TypeScript interface defining the shape of your data
- **createModel** — Creates a typed accessor model for store bindings
- **TextField** — A CxJS form widget with built-in two-way data binding
- **LabelsTopLayout** — A layout that positions labels above form fields
- **Button** with **onClick** — Access the `store` to read bound values
- **MsgBox.alert** — A built-in dialog for displaying messages
When the user types into the text field, the value is automatically written to the store via the `m.name` binding. The button's `onClick` handler reads this value from the store and displays a greeting.
---
# 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
```ts
import { Store } from 'cx/data';
```
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
```ts
import { Controller } from 'cx/ui';
```
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
```ts
import { HtmlElement } from 'cx/widgets';
```
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
```ts
import { PureContainer } from 'cx/ui';
```
`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
```ts
import { ContentResolver } from 'cx/widgets';
```
`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
```ts
import { createFunctionalComponent } from 'cx/ui';
```
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 (
);
```
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
```ts
import { Repeater } from 'cx/widgets';
```
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
}
/>
}
/>
);
```
## Validation Modes
The `validationMode` property controls how validation errors are displayed:
- **`tooltip`** (default) - Errors appear in a tooltip when hovering over the field
- **`help`** - Errors appear inline next to the field
- **`help-block`** - Errors appear as a block element below the field
```tsx
import { createModel } from "cx/data";
import { LabelsLeftLayout } from "cx/ui";
import { enableTooltips, TextField, ValidationGroup } from "cx/widgets";
enableTooltips();
interface Model {
tooltip: string;
help: string;
helpBlock: string;
}
const m = createModel();
export default (
);
```
## Custom Validation
Form fields accept validation callback functions through `onValidate`. Use `reactOn` to control when validation triggers.
```tsx
import { createModel } from "cx/data";
import { LabelsTopLayout } from "cx/ui";
import { TextField, ValidationGroup } from "cx/widgets";
interface Model {
framework: string;
}
const m = createModel();
export default (
{
if (v != "CxJS") return "Oops, wrong answer!";
}}
/>
);
```
## Async Validation
`onValidate` can return a Promise for server-side validation. Use `FirstVisibleChildLayout` to show either the error or a success indicator.
```tsx
import { createModel } from "cx/data";
import { FirstVisibleChildLayout, LabelsTopLayout } from "cx/ui";
import { TextField, ValidationGroup, ValidationError, Icon } from "cx/widgets";
import "../../icons/lucide";
interface Model {
username: string;
}
const m = createModel();
export default (
new Promise((resolve) => {
setTimeout(() => {
resolve(v == "cx" ? "This name is taken." : false);
}, 500);
})
}
help={
}
/>
);
```
---
# ValidationGroup
```ts
import { ValidationGroup } from 'cx/widgets';
```
The `ValidationGroup` is a container that tracks the validation state of all form fields inside it. It reports whether all fields are valid and can propagate settings like `disabled`, `readOnly`, or `viewMode` to all child fields.
```tsx
import { createModel } from "cx/data";
import { expr, LabelsTopLayout } from "cx/ui";
import { Button, MsgBox, TextField, ValidationGroup } from "cx/widgets";
interface Model {
data: {
firstName: string;
lastName: string;
email: string;
};
valid: boolean;
visited: boolean;
}
const m = createModel();
export default (
{
if (!store.get(m.valid)) {
store.set(m.visited, true);
return;
}
MsgBox.alert("Form submitted successfully!");
}}
>
Submit
v ? "Form is valid ✓" : "Please fill all required fields",
)}
/>
);
```
## Error Summary
The `errors` binding collects all validation errors from child fields. Use it with a `Repeater` to display an error summary.
```tsx
import { createModel } from "cx/data";
import { falsy, LabelsTopLayout, truthy } from "cx/ui";
import {
enableTooltips,
ValidationGroup,
NumberField,
Validator,
Repeater,
type ValidationErrorData,
} from "cx/widgets";
enableTooltips();
interface Model {
x: number;
y: number;
valid: boolean;
errors: ValidationErrorData[];
$error: ValidationErrorData;
}
const m = createModel();
export default (
Enter X and Y so that X + Y = 20.
{
if (x + y !== 20) return "X + Y must equal 20";
}}
visited
/>
Form is valid!
);
```
## Configuration
### Validation State
| Property | Type | Description |
| ---------- | ----------------------- | ------------------------------------------------------------------ |
| `valid` | `boolean` | Binding set to `true` if all child fields are valid |
| `invalid` | `boolean` | Binding set to `true` if any child field has a validation error |
| `errors` | `ValidationErrorData[]` | Binding to store the array of validation errors |
| `visited` | `boolean` | If `true`, forces all children to show validation errors |
| `isolated` | `boolean` | If `true`, isolates children from outer validation scopes |
### Field State Propagation
| Property | Type | Default | Description |
| --------------- | --------- | ------- | -------------------------------------------------------- |
| `disabled` | `boolean` | `false` | Disables all inner elements that support `disabled` |
| `enabled` | `boolean` | | Opposite of `disabled` |
| `readOnly` | `boolean` | `false` | Makes all inner elements read-only |
| `viewMode` | `boolean` | `false` | Switches all inner fields to view mode |
| `strict` | `boolean` | `false` | Forces children to respect group-level flags |
| `asterisk` | `boolean` | `false` | Adds red asterisk for all required fields |
| `tabOnEnterKey` | `boolean` | `false` | Moves focus to the next field when Enter is pressed |
---
# LookupField
```ts
import { LookupField } from 'cx/widgets';
```
The `LookupField` widget offers selection from a list of available options. It's similar to the native HTML `select` element but provides additional features:
- Searching/filtering the list
- Querying remote data
- User-friendly multiple selection with tags
```tsx
import { createModel } from "cx/data";
import { bind, LabelsTopLayout } from "cx/ui";
import { LookupField } from "cx/widgets";
interface Model {
selectedId: number;
selectedText: string;
selectedRecords: { id: number; text: string }[];
}
const m = createModel();
const options = [
{ id: 1, text: "Apple" },
{ id: 2, text: "Banana" },
{ id: 3, text: "Cherry" },
{ id: 4, text: "Date" },
{ id: 5, text: "Elderberry" },
];
export default (
);
```
## Remote Data
Use `onQuery` to fetch options from a server. The callback receives the search query and should return a list of options or a Promise.
Use `fetchAll` to fetch all data once and filter client-side, which is more efficient for moderate-sized datasets. Add `cacheAll` to keep the fetched data cached for the widget's lifetime, avoiding refetches when the dropdown reopens.
```tsx
import { createModel } from "cx/data";
import { LabelsTopLayout } from "cx/ui";
import { getSearchQueryPredicate } from "cx/util";
import { LookupField } from "cx/widgets";
interface Model {
cityId: number;
cityText: string;
cities: { id: number; text: string }[];
}
const m = createModel();
const cities = ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Diego", "Dallas"].map(
(text, id) => ({ id, text }),
);
function queryCity(query: string) {
let predicate = getSearchQueryPredicate(query);
return new Promise((resolve) => {
setTimeout(() => resolve(cities.filter((x) => predicate(x.text))), 300);
});
}
export default (
);
```
## Data Binding
| Selection Mode | Local (options) | Remote (onQuery) |
| -------------- | ---------------------------- | ---------------------- |
| Single | `value` and/or `text` | `value` and `text` |
| Multiple | `values` and/or `records` | `records` |
## Examples
- [Custom Bindings](/docs/examples/lookup-custom-bindings) - Passing additional fields to the selection
- [Infinite Lists](/docs/examples/lookup-infinite-list) - Handling large datasets with pagination
- [Options Filter](/docs/examples/lookup-options-filter) - Filtering options based on external criteria
- [Options Grouping](/docs/examples/lookup-options-grouping) - Grouping options in the dropdown
## Configuration
### Core Properties
| Property | Type | Default | Description |
| ---------------- | ---------- | ------- | --------------------------------------------------------- |
| `value` | `any` | | Selected value ID (single mode) |
| `text` | `string` | | Selected value text (single mode) |
| `values` | `array` | | Array of selected IDs (multiple mode) |
| `records` | `array` | | Array of selected records (multiple mode) |
| `options` | `array` | | Array of available options |
| `multiple` | `boolean` | `false` | Enable multiple selection |
| `placeholder` | `string` | | Placeholder text when empty |
| `disabled` | `boolean` | `false` | Disables the field |
| `readOnly` | `boolean` | `false` | Makes the field read-only |
### Option Fields
| Property | Type | Default | Description |
| ----------------- | -------- | -------- | ------------------------------------------------ |
| `optionIdField` | `string` | `"id"` | Field name for option ID |
| `optionTextField` | `string` | `"text"` | Field name for option display text |
| `valueIdField` | `string` | `"id"` | Field name for storing ID in selection |
| `valueTextField` | `string` | `"text"` | Field name for storing text in selection |
### Query Options
| Property | Type | Default | Description |
| ----------------- | ---------- | ------- | ----------------------------------------------------------- |
| `onQuery` | `function` | | `(query, instance) => options[]` - Fetch options |
| `queryDelay` | `number` | `150` | Delay in ms before query is executed |
| `minQueryLength` | `number` | `0` | Minimum characters before query is made |
| `fetchAll` | `boolean` | `false` | Fetch all options once, filter client-side |
| `cacheAll` | `boolean` | `false` | Cache fetched options for widget lifetime |
| `infinite` | `boolean` | `false` | Enable infinite scrolling for large datasets |
| `pageSize` | `number` | `100` | Number of items per page in infinite mode |
### Appearance
| Property | Type | Default | Description |
| -------------------------- | --------- | ------- | -------------------------------------------------- |
| `icon` | `string` | | Icon displayed on the left side |
| `showClear` | `boolean` | `true` | Shows clear button when value is present |
| `hideClear` | `boolean` | `false` | Hides the clear button |
| `alwaysShowClear` | `boolean` | `false` | Shows clear button even when required |
| `hideSearchField` | `boolean` | `false` | Hide the search input in dropdown |
| `minOptionsForSearchField` | `number` | `7` | Minimum options before search field is shown |
### Behavior
| Property | Type | Default | Description |
| ---------------- | --------- | ------- | ----------------------------------------------------- |
| `closeOnSelect` | `boolean` | `true` | Close dropdown after selection |
| `autoOpen` | `boolean` | `false` | Open dropdown on focus |
| `quickSelectAll` | `boolean` | `false` | Allow Ctrl+A to select all visible options |
| `sort` | `boolean` | `false` | Sort dropdown options alphabetically |
### Messages
| Property | Type | Default | Description |
| --------------------------- | -------- | ---------------------------------------- | --------------------------------- |
| `loadingText` | `string` | `"Loading..."` | Text shown while loading |
| `noResultsText` | `string` | `"No results found."` | Text when no options match |
| `queryErrorText` | `string` | `"Error occurred while querying..."` | Text on query error |
| `minQueryLengthMessageText` | `string` | `"Type in at least {0} character(s)."` | Text when query is too short |
---
# DateField
```ts
import { DateField } from 'cx/widgets';
```
The `DateField` widget is used for selecting dates. It supports both text input and picking from a dropdown calendar.
```tsx
import { createModel } from "cx/data";
import { bind, LabelsTopLayout } from "cx/ui";
import { DateField } from "cx/widgets";
interface Model {
date: string;
placeholder: string;
}
const m = createModel();
export default (
);
```
## Configuration
### Core Properties
| Property | Type | Default | Description |
| ------------- | --------- | ---------------- | -------------------------------------------------- |
| `value` | `string` | `null` | Selected date. Accepts Date objects or ISO strings |
| `format` | `string` | `"date;yyyyMMMdd"` | Display format. See [Formatting](/docs/intro/formatting) |
| `placeholder` | `string` | | Hint text displayed when the field is empty |
| `disabled` | `boolean` | `false` | Disables the input |
| `enabled` | `boolean` | | Opposite of `disabled` |
| `readOnly` | `boolean` | `false` | Makes the input read-only |
| `required` | `boolean` | `false` | Marks the field as required |
| `label` | `string` | | Field label text |
| `viewMode` | `boolean` | `false` | Display as plain text instead of interactive input |
| `emptyText` | `string` | | Text shown in view mode when the field is empty |
### Date Constraints
| Property | Type | Default | Description |
| -------------------- | ---------- | ------- | ---------------------------------------------------------------- |
| `minValue` | `string` | | Minimum selectable date |
| `minExclusive` | `boolean` | `false` | If `true`, the minimum date itself is not selectable |
| `maxValue` | `string` | | Maximum selectable date |
| `maxExclusive` | `boolean` | `false` | If `true`, the maximum date itself is not selectable |
| `disabledDaysOfWeek` | `number[]` | | Days of week that cannot be selected. Sunday is 0, Saturday is 6 |
### Validation
| Property | Type | Default | Description |
| ----------------------- | -------- | --------------------------------- | --------------------------------------------------------- |
| `minValueErrorText` | `string` | `"Select {0:d} or later."` | Error message when date is before minimum |
| `minExclusiveErrorText` | `string` | `"Select a date after {0:d}."` | Error message when date equals exclusive minimum |
| `maxValueErrorText` | `string` | `"Select {0:d} or before."` | Error message when date is after maximum |
| `maxExclusiveErrorText` | `string` | `"Select a date before {0:d}."` | Error message when date equals exclusive maximum |
| `inputErrorText` | `string` | `"Invalid date entered."` | Error message for invalid date input |
| `error` | `string` | | Custom error message. Field is marked invalid if set |
| `visited` | `boolean`| | If `true`, shows validation errors immediately |
| `validationMode` | `string` | `"tooltip"` | How to show errors: `"tooltip"`, `"help"`, `"help-block"` |
### Appearance
| Property | Type | Default | Description |
| --------------- | --------------- | ------------ | ----------------------------------------------- |
| `icon` | `string/object` | `"calendar"` | Icon on the left side |
| `showClear` | `boolean` | `true` | Shows a clear button when the field has a value |
| `hideClear` | `boolean` | `false` | Opposite of `showClear` |
| `alwaysShowClear` | `boolean` | `false` | Shows clear button even when field is required |
| `tooltip` | `string/object` | | Tooltip text or configuration |
| `inputStyle` | `string/object` | | CSS styles applied to the input element |
| `inputClass` | `string` | | CSS class applied to the input element |
### Behavior
| Property | Type | Default | Description |
| ----------------- | ---------- | -------------- | ------------------------------------------------------------------------------- |
| `autoFocus` | `boolean` | `false` | Automatically focuses the field on mount |
| `focusInputFirst` | `boolean` | `false` | Focus the text input instead of the calendar when opened |
| `partial` | `boolean` | `false` | Preserves time segment when combined with a time field |
| `encoding` | `function` | | Custom function to encode Date objects before storing. Default: `toISOString()` |
| `onParseInput` | `function` | | Custom parser for text input, e.g., to handle "today" |
| `dropdownOptions` | `object` | | Additional configuration for the dropdown |
### Callbacks
| Property | Type | Description |
| ------------ | ---------- | ------------------------------------------------------------------------------- |
| `onValidate` | `function` | `(value, instance, validationParams) => string|undefined` - Custom validation |
---
# List
```ts
import { List } from 'cx/widgets';
```
The `List` widget displays a collection of items and offers navigation and selection. It supports keyboard navigation, single or multiple selection, grouping, and sorting.
```tsx
import { createModel } from "cx/data";
import { Controller, KeySelection } from "cx/ui";
import { List } from "cx/widgets";
interface Item {
id: number;
text: string;
description: string;
}
interface Model {
records: Item[];
selection: number;
$record: Item;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(
m.records,
Array.from({ length: 5 }, (_, i) => ({
id: i + 1,
text: `Item ${i + 1}`,
description: `Description for item ${i + 1}`,
})),
);
}
}
export default (
);
```
## Configuration
### Core Properties
| Property | Type | Default | Description |
| -------------- | --------- | ----------- | -------------------------------------------------------- |
| `records` | `array` | | Array of records to display |
| `recordAlias` | `string` | `"$record"` | Alias used to expose record data in templates |
| `indexAlias` | `string` | `"$index"` | Alias used to expose record index |
| `emptyText` | `string` | | Text displayed when the list is empty |
| `selection` | `config` | | Selection configuration. See [Selections](/docs/concepts/selections) |
### Sorting
| Property | Type | Description |
| --------------- | -------- | ---------------------------------------------------------- |
| `sortField` | `string` | Binding to store the field name used for sorting |
| `sortDirection` | `string` | Binding to store sort direction (`"ASC"` or `"DESC"`) |
| `sorters` | `array` | Array of `{ field, direction }` objects for complex sorting |
| `sortOptions` | `object` | Options for `Intl.Collator` sorting |
### Appearance
| Property | Type | Description |
| ----------- | --------------- | ---------------------------------------- |
| `itemStyle` | `string/object` | CSS style applied to all list items |
| `itemClass` | `string/object` | CSS class applied to all list items |
| `grouping` | `config` | Grouping configuration for organizing items |
### Behavior
| Property | Type | Default | Description |
| ------------------------- | --------- | ------- | ----------------------------------------------------- |
| `scrollSelectionIntoView` | `boolean` | `false` | Auto-scroll selected item into view |
| `itemDisabled` | `boolean` | | Disable specific items |
| `selectMode` | `boolean` | `false` | Selection without cursor navigation |
| `selectOnTab` | `boolean` | `false` | Tab key selects item under cursor |
---
# Slider
```ts
import { Slider } from 'cx/widgets';
```
The `Slider` widget allows selecting numerical values by dragging the slider handle. It supports single values, ranges, stepping, mouse wheel control, and vertical orientation.
```tsx
import { createModel } from "cx/data";
import { bind, format, LabelsLeftLayout } from "cx/ui";
import { enableTooltips, Slider } from "cx/widgets";
enableTooltips();
interface Model {
value: number;
stepped: number;
from: number;
to: number;
}
const m = createModel();
export default (
);
```
## Configuration
### Value Properties
| Property | Type | Default | Description |
| ---------- | -------- | ------- | ---------------------------------------- |
| `value` | `number` | | Single value (alias for `to`) |
| `from` | `number` | | Low value for range slider |
| `to` | `number` | | High value for range slider |
| `minValue` | `number` | `0` | Minimum allowed value |
| `maxValue` | `number` | `100` | Maximum allowed value |
| `step` | `number` | | Rounding step for discrete values |
### Behavior
| Property | Type | Default | Description |
| ----------- | --------- | ------- | ----------------------------------------------------- |
| `wheel` | `boolean` | `false` | Enable mouse wheel control (not with range sliders) |
| `increment` | `number` | 1% | Value change per wheel tick |
| `disabled` | `boolean` | `false` | Disable the slider |
| `vertical` | `boolean` | `false` | Orient the slider vertically |
| `invert` | `boolean` | `false` | Invert vertical slider (top to bottom) |
### Appearance
| Property | Type | Description |
| ------------- | --------------- | ---------------------------------------- |
| `handleStyle` | `string/object` | CSS style for the slider handle |
| `rangeStyle` | `string/object` | CSS style for the selected range |
### Tooltips
| Property | Type | Description |
| -------------- | --------------- | ---------------------------------------- |
| `valueTooltip` | `string/object` | Tooltip config for the value handle |
| `fromTooltip` | `string/object` | Tooltip config for the `from` handle |
| `toTooltip` | `string/object` | Tooltip config for the `to` handle |
---
# LabeledContainer
```ts
import { LabeledContainer } from 'cx/widgets';
```
The `LabeledContainer` widget groups multiple form fields under a single label. This is useful for related fields like name parts (first/last) or date ranges (start/end).
```tsx
import { createModel } from "cx/data";
import { LabelsTopLayout } from "cx/ui";
import { LabeledContainer, TextField, DateField } from "cx/widgets";
interface PageModel {
firstName: string;
lastName: string;
startDate: string;
endDate: string;
}
const m = createModel();
export default (
);
```
Click on column headers to sort. Hold Ctrl (or Cmd on macOS) and click to select multiple rows.
## Columns
Columns define what data is displayed and how. The simplest column configuration uses the `field` property to display a record property:
```tsx
columns={[
{ header: "Name", field: "fullName" },
{ header: "Age", field: "age", align: "right" }
]}
```
For custom cell content, use the `children` property to render any widget inside the cell. The current record is available via `$record`:
```tsx
columns={[
{
header: "Name",
field: "fullName",
children:
},
{
header: "Actions",
children: (
{
edit(store.get(m.$record))
}}
/>
)
}
]}
```
Use `value` for computed display values while keeping `field` for sorting:
```tsx
{
header: "Status",
field: "active",
value: { expr: "{$record.active} ? 'Yes' : 'No'" }
}
```
### Headers
The `header` property can be a simple string or a configuration object for advanced scenarios like multi-row headers, custom tools, or spanning columns:
```tsx
columns={[
{
header: {
text: "Name",
colSpan: 2,
align: "center"
},
field: "fullName"
},
{
header: {
text: "Sales",
tool: ,
allowSorting: false
},
field: "sales"
}
]}
```
Use `colSpan` and `rowSpan` to create complex multi-row headers. The `tool` property allows adding custom components like filter dropdowns or menus inside the header.
### Dynamic Columns
Use `columnParams` and `onGetColumns` to dynamically generate columns based on data or user preferences:
```tsx
{
const baseColumns = [{ header: "Name", field: "name" }];
const dynamicColumns = params.map((col) => ({
header: col.label,
field: col.field,
}));
return [...baseColumns, ...dynamicColumns];
}}
/>
```
Whenever `columnParams` changes, `onGetColumns` is called to recalculate the column configuration. This is useful for user-configurable column visibility, pivot tables, or data-driven column generation.
See also: [Complex Headers](/docs/tables/complex-headers), [Column Resizing](/docs/tables/column-resizing), [Column Reordering](/docs/tables/column-reordering), [Fixed Columns](/docs/tables/fixed-columns), [Dynamic Columns](/docs/tables/dynamic-columns)
## Grouping
Grids support multi-level grouping with aggregates. Define grouping levels using the `grouping` property:
```tsx
```
Each grouping level supports the following options:
| Property | Type | Description |
| ------------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
| `key` | `object` | Object with name/selector pairs defining how records are grouped. Values are available as `$group.keyName`. |
| `showFooter` | `boolean` | Show a footer row with aggregate values after each group. |
| `showHeader` | `boolean` | Show a header row within each group. Useful for long printable reports. |
| `showCaption` | `boolean` | Show a caption row at the start of each group. Caption content is defined in the column's `caption` property. |
| `text` | `string` | A selector for text available as `$group.$name` in templates. |
| `comparer` | `function` | A function to determine group ordering. |
Column aggregates (`sum`, `count`, `avg`, `distinct`) are automatically calculated and available in footer/caption templates via `$group`.
For dynamic grouping, use `groupingParams` and `onGetGrouping`:
```tsx
{
if (!params) return null;
return [
{
key: { value: m.$record[params.field] },
showFooter: true,
},
];
}}
columns={columns}
/>
```
Whenever `groupingParams` changes, `onGetGrouping` is called to recalculate the grouping configuration. This allows users to change grouping at runtime.
See also: [Grouping](/docs/tables/grouping), [Dynamic Grouping](/docs/tables/dynamic-grouping)
## Drag & Drop
Grids support drag and drop for reordering rows, moving data between grids, and column reordering. Configure `dragSource` and `dropZone` to enable this functionality:
```tsx
{
// Handle the drop - reorder records
store.update(m.records, (records) => {
// Move record from e.source.recordIndex to e.target.insertionIndex
});
}}
columns={columns}
/>
```
The `dropZone.mode` can be `insertion` (shows insertion line between rows) or `preview` (highlights the target row). Use `onDropTest` to control which drops are allowed.
See also: [Row Drag and Drop](/docs/tables/row-drag-and-drop)
## Examples
- [Searching and Filtering](/docs/tables/searching-and-filtering)
- [Pagination](/docs/tables/pagination)
- [Multiple Selection](/docs/tables/multiple-selection)
- [Form Edit](/docs/tables/form-edit)
- [Row Editing](/docs/tables/row-editing)
- [Cell Editing](/docs/tables/cell-editing)
- [Inline Edit](/docs/tables/inline-edit)
- [Tree Grid](/docs/tables/tree-grid)
- [Stateful TreeGrid](/docs/tables/stateful-tree-grid)
- [Header Menu](/docs/tables/header-menu)
- [Buffering](/docs/tables/buffering)
- [Infinite Scrolling](/docs/tables/infinite-scrolling)
- [Row Expanding](/docs/tables/row-expanding)
## Grid Configuration
### Data
| Property | Type | Description |
| --------------- | ---------- | ---------------------------------------------------------------------------------------------- |
| `records` | `array` | An array of records to be displayed in the grid. |
| `keyField` | `string` | Field used as unique record identifier. Improves performance on sort by tracking row movement. |
| `columns` | `array` | An array of column configurations. See Column Configuration below. |
| `columnParams` | `any` | Parameters passed to `onGetColumns` for dynamic column generation. |
| `onGetColumns` | `function` | Callback to dynamically generate columns when `columnParams` changes. |
| `recordName` | `string` | Record binding alias. Default is `$record`. |
| `recordAlias` | `string` | Alias for `recordName`. |
| `dataAdapter` | `object` | Data adapter configuration for grouping and tree operations. |
| `emptyText` | `string` | Text displayed when grid has no records. |
### Selection
| Property | Type | Description |
| ---------------------------- | ---------- | --------------------------------------------------------------------------------- |
| `selection` | `object` | Selection configuration. See [Selections](/docs/concepts/selections) for details. |
| `scrollSelectionIntoView` | `boolean` | Scroll selected row into view. Default is `false`. |
| `onCreateIsRecordSelectable` | `function` | Callback to create a function that checks if a record is selectable. |
### Sorting
| Property | Type | Description |
| ---------------------- | --------- | ------------------------------------------------------------------------ |
| `sortField` | `string` | Binding to store the name of the field used for sorting. |
| `sortDirection` | `string` | Binding to store the sort direction (`ASC` or `DESC`). |
| `sorters` | `array` | Binding to store multiple sorters for server-side sorting. |
| `preSorters` | `array` | Sorters prepended to the actual list of sorters. |
| `defaultSortField` | `string` | Default sort field when no sorting is set. |
| `defaultSortDirection` | `string` | Default sort direction (`ASC` or `DESC`). |
| `remoteSort` | `boolean` | Set to `true` if sorting is done server-side. |
| `clearableSort` | `boolean` | Allow clearing sort by clicking header a third time. |
| `sortOptions` | `object` | Collator options for sorting. See MDN Intl.Collator for available opts. |
### Grouping
| Property | Type | Description |
| ------------------- | ---------- | ------------------------------------------------------------- |
| `grouping` | `array` | An array of grouping level definitions. |
| `groupingParams` | `any` | Parameters passed to `onGetGrouping` for dynamic grouping. |
| `onGetGrouping` | `function` | Callback to dynamically generate grouping when params change. |
| `preserveGroupOrder`| `boolean` | Keep groups in the same order as source records. |
### Filtering
| Property | Type | Description |
| ---------------- | ---------- | -------------------------------------------------------- |
| `filterParams` | `any` | Parameters passed to `onCreateFilter` callback. |
| `onCreateFilter` | `function` | Callback to create a filter predicate from filterParams. |
### Appearance
| Property | Type | Description |
| ------------ | ----------------- | ------------------------------------------------------------------------------------------------------- |
| `scrollable` | `boolean` | Set to `true` to add a vertical scroll and fixed header. Grid should have `height` or `max-height` set. |
| `border` | `boolean` | Set to `true` to add default border. Automatically set if `scrollable`. |
| `vlines` | `boolean` | Set to `true` to add vertical gridlines. |
| `headerMode` | `string` | Header appearance: `plain` or `default`. |
| `showHeader` | `boolean` | Show grid header within groups. Useful for long printable grids. Default is `false`. |
| `showFooter` | `boolean` | Show grid footer. Default is `false`. |
| `fixedFooter`| `boolean` | Set to `true` to add a fixed footer at the bottom of the grid. |
| `rowClass` | `string` | Additional CSS class to be added to each grid row. |
| `rowStyle` | `string \| object`| Additional CSS styles to be added to each grid row. |
### Performance
| Property | Type | Description |
| --------------------- | --------- | -------------------------------------------------------------------------------- |
| `cached` | `boolean` | Set to `true` to enable row caching for better performance. |
| `buffered` | `boolean` | Set to `true` to render only visible rows. Requires `scrollable`. |
| `bufferSize` | `number` | Number of rendered rows in buffered grids. Default is `60`. |
| `bufferStep` | `number` | Number of rows to scroll before buffer recalculation. |
| `measureRowHeights` | `boolean` | Cache variable row heights in buffered grids. Default is `false`. |
| `lockColumnWidths` | `boolean` | Lock column widths after first render. Useful for pagination. |
| `preciseMeasurements` | `boolean` | Enable sub-pixel measurements. Useful for grids with many columns or small zoom. |
### Infinite Scrolling
| Property | Type | Description |
| ---------------- | ---------- | ----------------------------------------------------- |
| `infinite` | `boolean` | Enable infinite scrolling. |
| `onFetchRecords` | `function` | Callback to fetch records during infinite loading. |
### Cell Editing
| Property | Type | Description |
| ------------------ | ---------- | -------------------------------------------------------------------------- |
| `cellEditable` | `boolean` | Set to `true` to enable cell editing. Columns must specify `editor` field. |
| `onBeforeCellEdit` | `function` | Callback before cell edit. Return `false` to prevent edit mode. |
| `onCellEdited` | `function` | Callback after a cell has been successfully edited. |
### Focus & Hover
| Property | Type | Description |
| -------------- | ------------------- | ------------------------------------------------------------------------- |
| `focusable` | `boolean` | Set to `true` or `false` to explicitly control if grid can receive focus. |
| `hoverChannel` | `string` | Identifier for hover effect synchronization across components. |
| `rowHoverId` | `string \| number` | Unique record identifier within the hover sync group. |
### Row Callbacks
| Property | Type | Description |
| ------------------- | ---------- | -------------------------------------------------- |
| `onRowClick` | `function` | Callback executed when a row is clicked. |
| `onRowDoubleClick` | `function` | Callback executed when a row is double-clicked. |
| `onRowContextMenu` | `function` | Callback executed when a row is right-clicked. |
| `onRowKeyDown` | `function` | Callback executed on key down in a focused row. |
### Column Callbacks
| Property | Type | Description |
| ---------------------- | ---------- | --------------------------------------------------- |
| `onColumnContextMenu` | `function` | Callback executed when a column header is right-clicked. |
| `onColumnResize` | `function` | Callback executed after a column has been resized. |
### Other
| Property | Type | Description |
| --------------------- | ---------- | --------------------------------------------------------------- |
| `scrollResetParams` | `any` | Parameters whose change will reset scroll position. |
| `onTrackMappedRecords`| `function` | Callback to track and retrieve displayed (filtered) records. |
| `onRef` | `function` | Callback to get grid component and instance references on init. |
## Column Configuration
| Property | Type | Description |
| ---------------------- | ------------------ | ------------------------------------------------------- |
| `field` | `string` | Name of the property inside the record to display. |
| `header` | `string \| object` | Text or configuration object for the column header. |
| `format` | `string` | Format string for cell values. |
| `align` | `string` | Column alignment: `left`, `right`, or `center`. |
| `sortable` | `boolean` | Set to `true` to make the column sortable. |
| `sortField` | `string` | Alternative field used for sorting. |
| `primarySortDirection` | `string` | Initial sort direction on first click: `ASC` or `DESC`. |
| `aggregate` | `string` | Aggregate function: `sum`, `count`, `distinct`, `avg`. |
| `aggregateField` | `string` | Field used for aggregation if different from `field`. |
| `footer` | `string` | Value to render in the footer. |
| `editable` | `boolean` | Indicate if cell is editable. Default is `true`. |
| `editor` | `object` | Cell editor configuration. |
| `draggable` | `boolean` | Make column draggable for reordering. |
| `resizable` | `boolean` | Make column resizable. |
| `mergeCells` | `string` | Merge adjacent cells: `same-value` or `always`. |
## Column Header Configuration
When `header` is an object, the following properties are available:
| Property | Type | Description |
| -------------- | ------------------ | ------------------------------------------------------------------- |
| `text` | `string` | Header text. |
| `align` | `string` | Header text alignment: `left`, `right`, or `center`. |
| `allowSorting` | `boolean` | Enable or disable sorting on the column. Default is `true`. |
| `colSpan` | `number` | Number of columns the header cell should span. Default is `1`. |
| `rowSpan` | `number` | Number of rows the header cell should span. Default is `1`. |
| `tool` | `object` | A component rendered inside the header for custom menus or filters. |
| `resizable` | `boolean` | Set to `true` to make the column resizable. |
| `width` | `number` | Binding to store column width after resize. |
| `tooltip` | `string \| object` | Tooltip configuration for the header. |
## Drag & Drop Configuration
| Property | Type | Description |
| ----------------- | --------- | ----------------------------------------------------------------------------------- |
| `dragSource` | `object` | Drag source configuration. Define `mode` as `move` or `copy` and additional `data`. |
| `dropZone` | `object` | Drop zone configuration. Define `mode` as `insertion` or `preview`. |
| `allowsFileDrops` | `boolean` | Allow grid to receive drag and drop operations containing files. |
### Drag & Drop Callbacks
**`onDragStart(e: DragEvent, instance: Instance)`** - Called when the user starts dragging a row.
- `e` - The native drag event.
- `instance` - The grid instance.
**`onDragEnd(e: DragEvent, instance: Instance)`** - Called when the user stops dragging.
- `e` - The native drag event.
- `instance` - The grid instance.
**`onDragOver(e: GridDragEvent, instance: Instance): boolean?`** - Called when the user drags over another item. Return `false` to prevent dropping.
- `e` - Grid drag event containing `source` and `target` with record indices.
- `instance` - The grid instance.
**`onDrop(e: GridDragEvent, instance: Instance)`** - Called when a drop occurs. Use it to update data such as rearranging the list.
- `e` - Grid drag event containing `source` and `target` with record indices and insertion index.
- `instance` - The grid instance.
**`onDropTest(e: DragEvent, instance: Instance): boolean`** - Checks whether a drop action is allowed. Return `false` to reject.
- `e` - The native drag event.
- `instance` - The grid instance.
**`onRowDragOver(e: GridRowDragEvent, instance: Instance): boolean?`** - Called when dragging over a specific row. Useful for tree grids.
- `e` - Grid row drag event containing source record and target row info.
- `instance` - The grid instance.
**`onRowDrop(e: GridRowDragEvent, instance: Instance): boolean?`** - Called when dropping onto a row. Use it to attach a node to a subtree.
- `e` - Grid row drag event containing source record and target row info.
- `instance` - The grid instance.
**`onRowDropTest(e: DragEvent, instance: Instance): boolean`** - Checks whether a row drop action is allowed.
- `e` - The native drag event.
- `instance` - The grid instance.
**`onColumnDrop(e: GridColumnDropEvent, instance: Instance): boolean?`** - Called when a column is dropped. Use it for column reordering.
- `e` - Column drop event containing column info.
- `instance` - The grid instance.
**`onColumnDropTest(e: DragEvent, instance: Instance): boolean`** - Checks whether a column drop is valid.
- `e` - The native drag event.
- `instance` - The grid instance.
**`onCreateIsRecordDraggable(): (record) => boolean`** - Returns a predicate function that determines which records can be dragged.
---
# Searching and Filtering
```ts
import { getSearchQueryPredicate } from 'cx/util';
```
Grids support client-side filtering using `filterParams` and `onCreateFilter`. This is ideal for filtering data already loaded in the browser.
```tsx
import { createModel } from "cx/data";
import { Controller } from "cx/ui";
import { Grid, LookupField, TextField } from "cx/widgets";
import { getSearchQueryPredicate } from "cx/util";
import "../../icons/lucide";
interface Employee {
id: number;
fullName: string;
phone: string;
city: string;
}
interface PageModel {
search: string;
cityFilter: string;
employees: Employee[];
cities: { id: string; text: string }[];
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.employees, [
{ id: 1, fullName: "Alice Johnson", phone: "555-1234", city: "New York" },
{ id: 2, fullName: "Bob Smith", phone: "555-2345", city: "Los Angeles" },
{ id: 3, fullName: "Carol White", phone: "555-3456", city: "Chicago" },
{ id: 4, fullName: "David Brown", phone: "555-4567", city: "Houston" },
{ id: 5, fullName: "Eva Green", phone: "555-5678", city: "Phoenix" },
{ id: 6, fullName: "Frank Miller", phone: "555-6789", city: "New York" },
{ id: 7, fullName: "Grace Lee", phone: "555-7890", city: "Chicago" },
{
id: 8,
fullName: "Henry Wilson",
phone: "555-8901",
city: "Los Angeles",
},
]);
this.store.set(m.cities, [
{ id: "New York", text: "New York" },
{ id: "Los Angeles", text: "Los Angeles" },
{ id: "Chicago", text: "Chicago" },
{ id: "Houston", text: "Houston" },
{ id: "Phoenix", text: "Phoenix" },
]);
}
}
export default (
{
let { search, city } = params || {};
let predicate = search ? getSearchQueryPredicate(search) : null;
return (record: Employee) => {
if (city && record.city !== city) return false;
if (predicate) {
return (
predicate(record.fullName) ||
predicate(record.phone) ||
predicate(record.city)
);
}
return true;
};
}}
/>
);
```
Type in the search field to filter records across all fields. Use the city dropdown to filter by a specific city. Both filters can be combined.
## How It Works
The `filterParams` property holds the current filter state. It can be a single value or an object with multiple filter criteria. Whenever it changes, `onCreateFilter` is called to create a new predicate function:
```tsx
{
let { search, city } = params || {};
let predicate = search ? getSearchQueryPredicate(search) : null;
return (record) => {
if (city && record.city !== city) return false;
if (predicate) {
return (
predicate(record.fullName) ||
predicate(record.phone) ||
predicate(record.city)
);
}
return true;
};
}}
/>
```
The `getSearchQueryPredicate` utility creates a case-insensitive predicate that supports multiple search terms. Records pass the filter if any field matches all search terms.
## Configuration
| Property | Type | Description |
| -------- | ---- | ----------- |
| `filterParams` | `Prop` | Parameters passed to `onCreateFilter` when they change. |
| `onCreateFilter` | `function` | Callback that receives `filterParams` and returns a predicate function `(record) => boolean`. |
| `emptyText` | `string` | Text displayed when no records match the filter. |
---
# Pagination
```ts
import { Grid, Pagination } from 'cx/widgets';
```
Grid is commonly used with server-side pagination, sorting, and filtering. The `Pagination` widget provides navigation controls, while reactive triggers manage data fetching.
```tsx
import { createModel, getComparer } from "cx/data";
import { Controller, Sorter } from "cx/ui";
import { Grid, Pagination, Select, TextField } from "cx/widgets";
interface Employee {
id: number;
fullName: string;
phone: string;
city: string;
}
interface Filter {
name: string | null;
phone: string | null;
city: string | null;
}
interface PageModel {
records: Employee[];
page: number;
pageSize: number;
pageCount: number;
sorters: Sorter[];
filter: Filter;
}
const m = createModel();
class PageController extends Controller {
dataSet: Employee[] = [];
onInit() {
// Generate sample data
this.dataSet = Array.from({ length: 200 }, (_, i) => ({
id: i + 1,
fullName:
[
"Alice Johnson",
"Bob Smith",
"Carol White",
"David Brown",
"Eva Green",
][i % 5] +
" " +
(i + 1),
phone: `555-${String(1000 + i).padStart(4, "0")}`,
city: ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix"][i % 5],
}));
this.store.init(m.pageSize, 10);
this.store.init(m.filter, { name: null, phone: null, city: null });
// Reset to page 1 when context changes
this.addTrigger(
"resetPage",
[m.pageSize, m.sorters, m.filter],
() => {
this.store.set(m.page, 1);
},
true,
);
// Fetch data when pagination parameters change
this.addTrigger(
"fetchData",
[m.pageSize, m.page, m.sorters, m.filter],
(size, page, sorters, filter) => {
// Simulate server-side filtering
let filtered = this.dataSet;
if (filter) {
if (filter.name) {
const checks = filter.name
.split(" ")
.map((w) => new RegExp(w, "gi"));
filtered = filtered.filter((x) =>
checks.every((ex) => x.fullName.match(ex)),
);
}
if (filter.phone) {
filtered = filtered.filter((x) => x.phone.includes(filter.phone!));
}
if (filter.city) {
filtered = filtered.filter((x) =>
x.city.toLowerCase().includes(filter.city!.toLowerCase()),
);
}
}
// Simulate server-side sorting
if (sorters?.length) {
const compare = getComparer(
sorters.map((s) => ({
value: { bind: s.field },
direction: s.direction,
})),
);
filtered = [...filtered].sort(compare);
}
// Simulate server-side pagination
this.store.set(
m.records,
filtered.slice((page - 1) * size, page * size),
);
this.store.set(m.pageCount, Math.ceil(filtered.length / size));
},
true,
);
}
}
export default (
);
```
Click the arrows to expand/collapse nodes. The tree loads child nodes lazily when expanded.
## TreeAdapter
To display hierarchical data, configure the `dataAdapter` property with `TreeAdapter`:
```tsx
```
If your data is already a tree structure with children nested in each record, that's all you need. The `TreeAdapter` will flatten the tree for display and manage expand/collapse state.
For lazy loading, implement the `onLoad` callback to fetch children when a node is expanded:
```tsx
dataAdapter={{
type: TreeAdapter,
onLoad: (context, instance, node) =>
instance.getControllerByType(PageController).loadChildren(node),
}}
```
Return an array of child records or a Promise that resolves to an array.
## TreeNode
Use [TreeNode](/docs/tables/tree-node) in the column's `children` to render the expand/collapse arrow and indentation:
```tsx
{
header: "Name",
field: "name",
children: (
)
}
```
The `TreeAdapter` manages special properties on each record (`$expanded`, `$leaf`, `$level`, `$loading`) that TreeNode uses for rendering.
See also: [TreeNode](/docs/tables/tree-node), [Stateful TreeGrid](/docs/tables/stateful-tree-grid), [Searching Tree Grids](/docs/tables/searching-tree-grids)
---
# Svg
```ts
import { Svg } from 'cx/svg';
```
CxJS has excellent support for _Scalable Vector Graphics_ (SVG) and enables responsive layouts and charts using the concept of **bounded objects**.
```tsx
import { Svg, Rectangle } from "cx/svg";
export default (
);
```
The `Svg` component serves as a container for SVG elements. You can mix two approaches:
- **Native SVG elements** (`rect`, `ellipse`, `line`, `text`) — positioned with standard attributes like `x`, `y`, `width`, `height`, but you must calculate positions manually
- **CxJS components** (`Rectangle`, `Ellipse`, `Line`, `Text`) — work as **bounded objects** that automatically adapt to their container size using `anchors`, `offset`, and `margin` properties
## Bounded Objects
> Use the `Svg` container instead of the native `svg` element to enable bounded objects.
The `Svg` component measures its size and passes bounding box information to child elements. Child elements use parent bounds to calculate their own size and pass it to their children.
Bounds are defined using the `anchors`, `offset`, and `margin` properties. Each property consists of four components `t r b l` (top, right, bottom, left) in clockwise order. Do not use suffixes like `%` or `px`.
### Anchors
Anchors define how child bounds are tied to the parent:
- `0` aligns to the left/top edge
- `1` aligns to the right/bottom edge
- Values between `0` and `1` position proportionally
```tsx
import { Svg, Rectangle, Text } from "cx/svg";
export default (
);
```
### Offset and Margin
The `offset` property translates the edges of the bounding box. It always works in the top-to-bottom and left-to-right direction, so use negative values for right and bottom edges.
The `margin` property works like CSS margin — positive values shrink the box inward, negative values expand it outward.
```tsx
import { Svg, Rectangle, Text } from "cx/svg";
export default (
);
```
## Aspect Ratio
When you can't give fixed dimensions to an SVG element, use `aspectRatio` with `autoHeight` or `autoWidth` to automatically size the element based on available space.
```tsx
import { Svg, Rectangle } from "cx/svg";
export default (
);
```
In this example, the height is automatically calculated to be 4 times smaller than the width.
## Configuration
| Property | Type | Description |
| ------------- | --------------- | ----------------------------------------------------------- |
| `anchors` | `string/number` | Defines how bounds are tied to parent. Format: `"t r b l"`. |
| `offset` | `string/number` | Translates edges of the bounding box. Format: `"t r b l"`. |
| `margin` | `string/number` | Applies margin to boundaries (CSS-like behavior). |
| `padding` | `string/number` | Padding applied before passing bounds to children. |
| `aspectRatio` | `number` | Aspect ratio (width/height). Default: `1.618`. |
| `autoWidth` | `boolean` | Calculate width from height and aspect ratio. |
| `autoHeight` | `boolean` | Calculate height from width and aspect ratio. |
---
# Charts Overview
```ts
import { Chart, LineGraph, PieChart } from 'cx/charts';
```
Charts are an important part of CxJS, extending the SVG package. Rather than providing pre-built chart widgets for each type, CxJS focuses on low-level components that can be assembled into any chart. This gives developers full control over appearance and behavior.
- **Line, bar, and scatter charts** are defined inside a `Chart` widget
- **Pie charts** use the `PieChart` widget
## Basic Chart
The `Chart` widget defines axes and bounds for two-dimensional charts.
```tsx
import { Svg, Rectangle } from "cx/svg";
import { Chart, Gridlines, Legend, LineGraph, NumericAxis } from "cx/charts";
export default (
);
```
The most important part is **axis configuration**. Numeric, category, and time axis types are supported.
Charts consist of multiple child elements. In the example above:
- `Rectangle` provides a white background
- `Gridlines` adds grid lines
- `LineGraph` presents the data
Child elements inherit axis information from the chart and use it for positioning.
## Pie Charts
```tsx
import { createModel } from "cx/data";
import { bind, KeySelection, Repeater } from "cx/ui";
import { Svg } from "cx/svg";
import { ColorMap, Legend, PieChart, PieSlice } from "cx/charts";
interface SliceData {
name: string;
value: number;
}
interface Model {
$record: SliceData;
selected: string;
}
const m = createModel();
export default (
);
```
The `Repeater` widget iterates over an array and maps it to chart elements. A selection model enables interaction with other widgets on the page.
## Legends
```ts
import { Legend } from 'cx/charts';
```
Legends are context-aware — all legend-aware widgets report information about themselves to populate the legend. Widgets can also report legend actions, making the legend a toggle or selection switch.
Key points:
- `Legend` is **not** SVG-based and should be placed **outside** the `Svg` widget
- `Legend.Scope` delimits legend scopes when using multiple charts
- Use unique legend names to separate multiple legends
## Color Palettes
CxJS includes a standard palette of 16 colors based on [Google Material Design](https://material.google.com/style/color.html). Colors support hover, selection, and disabled states.
```tsx
export default (
);
```
The `ColorMap` utility assigns different colors to chart elements with the same `colorMap` attribute, maximizing color distance between elements.
## Main Components
**Chart Types:**
- [LineGraph](/docs/charts/line-graph) — Line and area charts
- [BarGraph](/docs/charts/bar-graph) — Horizontal bar charts
- [ColumnGraph](/docs/charts/column-graph) — Vertical column charts
- [ScatterGraph](/docs/charts/scatter-graph) — Scatter plots
- [PieChart](/docs/charts/pie-chart) — Pie and donut charts
**Axes:**
- [NumericAxis](/docs/charts/numeric-axis) — For numerical data
- [CategoryAxis](/docs/charts/category-axis) — For categorical data
- [TimeAxis](/docs/charts/time-axis) — For date/time data
**Utilities:**
- [Legend](/docs/charts/legend) — Chart legends
- [Gridlines](/docs/charts/gridlines) — Background grid
- [ColorMap](/docs/charts/color-map) — Automatic color assignment
---
# Chart
```ts
import { Chart } from 'cx/charts';
```
The `Chart` widget defines axes and bounds for two-dimensional charts such as line, scatter, bar, and column charts.
```tsx
import { Svg, Rectangle } from "cx/svg";
import { Chart, NumericAxis, Gridlines } from "cx/charts";
export default (
);
```
## Axis Configuration
The most important part is **axis configuration**. A chart typically needs two axes:
- **Horizontal axis** (x) — default orientation
- **Vertical axis** (y) — set `vertical: true`
Numeric, category, and time axis types are supported. One axis must be vertical for proper chart orientation.
## Margin and Offset
The `margin` property on the `Chart` element reserves space for axis labels and ticks. Use the format `"top right bottom left"` (e.g., `margin="10 20 30 50"`).
The `offset` property works similarly to `margin` but with different sign conventions — it always works in the top-to-bottom and left-to-right direction. For right and bottom offsets, use negative numbers.
Example: `offset="10 -20 -30 50"` is equivalent to `margin="10 20 30 50"`.
## Secondary Axes
Charts can have secondary axes displayed at the top (x2) or right (y2) side. Set `secondary: true` on the axis configuration.
```tsx
import { Svg, Rectangle } from "cx/svg";
import { Chart, NumericAxis, Gridlines } from "cx/charts";
export default (
);
```
Key axis properties:
- `secondary` — Displays axis at top/right instead of bottom/left
- `inverted` — Reverses the axis direction (values in descending order)
- `vertical` — Required for y-axes
> Be careful with gridlines when using secondary axes — you may need separate `Gridlines` components for each axis pair.
## Main Chart Elements
### Axes
- [NumericAxis](/docs/charts/numeric-axis) — For numerical data
- [CategoryAxis](/docs/charts/category-axis) — For categorical data
- [TimeAxis](/docs/charts/time-axis) — For date/time data
### Graphs (Series)
- [LineGraph](/docs/charts/line-graph) — Line and area charts
- [BarGraph](/docs/charts/bar-graph) — Horizontal bar series
- [ColumnGraph](/docs/charts/column-graph) — Vertical column series
- [ScatterGraph](/docs/charts/scatter-graph) — Scatter plots
### Individual Elements
- [Column](/docs/charts/column) — Single column in column charts (use when columns differ)
- [Bar](/docs/charts/bar) — Single bar in bar charts (use when bars differ)
- [Marker](/docs/charts/marker) — Point markers for scatter charts, supports dragging
- [MarkerLine](/docs/charts/marker-line) — Highlight specific values (e.g., min/max)
- [Range](/docs/charts/range) — Draw rectangular areas (zones)
### SVG Elements
- [Rectangle](/docs/charts/rectangle) — Draw rectangles
- [Line](/docs/charts/line) — Draw lines
## Configuration
| Property | Type | Description |
|----------|------|-------------|
| `axes` | `object` | Axis definitions. Each key represents an axis name, each value holds axis configuration. |
| `margin` | `string` | Space reserved for axis labels in `"top right bottom left"` format. |
| `offset` | `string` | Alternative to margin using top-to-bottom, left-to-right direction. Use negative values for right/bottom. |
| `axesOnTop` | `boolean` | Set to `true` to render axes on top of the data series. |
---
# NumericAxis
```ts
import { NumericAxis } from 'cx/charts';
```
The `NumericAxis` component maps numeric data along the horizontal or vertical axis of a chart. Graph components use it to calculate their position, and the axis adapts its visible range to the data being shown. The axis is also responsive—different tick configurations are selected based on available space.
This example shows four axes: primary X and Y axes with explicit ranges, and secondary inverted axes on the opposite sides.
```tsx
import { Svg, Rectangle } from "cx/svg";
import { Chart, NumericAxis, Gridlines } from "cx/charts";
export default (
);
```
Be careful with gridlines when using secondary axes as they may overlap.
## Configuration
| Property | Type | Description |
| ------------------ | --------- | ------------------------------------------------------------------------------------------------- |
| `min` | `number` | Minimum value. |
| `max` | `number` | Maximum value. |
| `vertical` | `boolean` | Set to `true` for a vertical (Y) axis. |
| `secondary` | `boolean` | Set to `true` to position the axis on the opposite side (right for vertical, top for horizontal). |
| `inverted` | `boolean` | Set to `true` to invert the axis direction. |
| `snapToTicks` | `number` | Range alignment: `0` = lowest ticks, `1` = medium ticks (default), `2` = major ticks. |
| `normalized` | `boolean` | Set to `true` to normalize the input range (0-1). |
| `format` | `string` | Value format for labels. Default is `"n"`. |
| `labelDivisor` | `number` | Divide values by this number before rendering labels. Default is `1`. |
| `deadZone` | `number` | Size of a zone reserved for labels at both ends of the axis. |
| `upperDeadZone` | `number` | Size of a zone reserved for labels near the upper end of the axis. |
| `lowerDeadZone` | `number` | Size of a zone reserved for labels near the lower end of the axis. |
| `minLabelTickSize` | `number` | Minimum value increment between labels. Set to `1` for integer axes to avoid duplicate labels. |
---
# CategoryAxis
```ts
import { CategoryAxis } from 'cx/charts';
```
The `CategoryAxis` component maps discrete data values along the horizontal or vertical axis of a chart. It's commonly used with bar charts and scatter plots where data points belong to named categories.
Categories are automatically discovered from the data. In this example, markers define both X and Y categories through their position values.
```tsx
import { Svg, Rectangle } from "cx/svg";
import { Chart, CategoryAxis, Gridlines, Marker } from "cx/charts";
export default (
);
```
## Configuration
| Property | Type | Description |
| --------------- | ------------------ | --------------------------------------------------------------------------------- |
| `vertical` | `boolean` | Set to `true` for a vertical (Y) axis. |
| `secondary` | `boolean` | Set to `true` to position the axis on the opposite side. |
| `inverted` | `boolean` | Set to `true` to invert the axis direction. |
| `uniform` | `boolean` | Uniform axes provide exact size and offset for all entries. |
| `values` | `array` / `object` | Values used to initialize the axis. Object keys become values, values are labels. |
| `names` | `array` / `object` | Names (labels) corresponding to the given values. |
| `minSize` | `number` | Minimum number of category slots. |
| `categoryCount` | `binding` | Output binding for category count. |
| `format` | `string` | Additional label formatting. |
---
# LineGraph
```ts
import { LineGraph } from 'cx/charts';
```
Line charts are commonly used for data trends visualization. The `LineGraph` widget renders a series of data points connected by line segments, with optional area fill, smoothing, and stacking capabilities.
```tsx
import { createModel } from "cx/data";
import {
bind,
Controller,
expr,
LabelsTopLayout,
LabelsTopLayoutCell,
} from "cx/ui";
import { Slider, Switch } from "cx/widgets";
import { Svg } from "cx/svg";
import { Chart, Gridlines, Legend, LineGraph, NumericAxis } from "cx/charts";
interface DataPoint {
x: number;
y: number | null;
y2: number;
y2l: number;
y2h: number;
}
interface Model {
points: DataPoint[];
pointsCount: number;
showArea: boolean;
showLine: boolean;
smooth: boolean;
smoothingRatio: number;
line1: boolean;
line2: boolean;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.init(m.pointsCount, 50);
this.store.init(m.showArea, true);
this.store.init(m.showLine, true);
this.store.init(m.smooth, true);
this.store.init(m.smoothingRatio, 0.07);
this.addTrigger(
"on-count-change",
[m.pointsCount],
(cnt) => {
let y1 = 250,
y2 = 350;
this.store.set(
m.points,
Array.from({ length: cnt }, (_, i) => ({
x: i * 4,
y: i % 20 === 3 ? null : (y1 = y1 + (Math.random() - 0.5) * 30),
y2: (y2 = y2 + (Math.random() - 0.5) * 30),
y2l: y2 - 50,
y2h: y2 + 50,
})),
);
},
true,
);
}
}
export default (
`${v} points`)}
/>
v?.toFixed(2) ?? "")}
/>
);
```
The example above demonstrates multiple line series with area fill, smooth curves, and a range band (using `y0Field` and `yField` to define upper and lower bounds). Use the controls to adjust the number of data points, toggle area/line visibility, and modify the smoothing effect.
## Configuration
### Data Properties
| Property | Type | Default | Description |
| --------- | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `data` | `array` | | Data for the graph. Each entry should be an object with at least two properties whose names should match `xField` and `yField` |
| `xField` | `string` | `"x"` | Name of the property which holds the x value |
| `yField` | `string` | `"y"` | Name of the property which holds the y value |
| `y0Field` | `string/false` | `false` | Name of the property which holds the y0 (base) value. Used for range/band charts |
| `y0` | `number` | `0` | Base value used for area charts when `y0Field` is not specified |
### Appearance
| Property | Type | Default | Description |
| ---------------- | --------------- | ------- | ---------------------------------------------------------------------------------------- |
| `colorIndex` | `number` | | Index of a color from the standard palette (0-15) |
| `colorMap` | `string` | | Used to automatically assign a color based on `name` and the contextual `ColorMap` widget |
| `colorName` | `string` | | Name used to resolve the color. If not provided, `name` is used instead |
| `line` | `boolean` | `true` | Show the line connecting data points. Set to `false` to hide |
| `lineStyle` | `string/object` | | Additional styles applied to the line element |
| `area` | `boolean` | `false` | Fill the area under the line |
| `areaStyle` | `string/object` | | Additional styles applied to the area element |
| `hiddenBase` | `boolean` | `false` | If `true`, clips the base of the graph to show only the value range |
### Smoothing
| Property | Type | Default | Description |
| ---------------- | --------- | ------- | ---------------------------------------------------------------------------------------------- |
| `smooth` | `boolean` | `false` | Set to `true` to draw smoothed lines using cubic Bézier curves |
| `smoothingRatio` | `number` | `0.05` | Controls the intensity of smoothing (0 to 0.4). Higher values create more curved lines |
### Stacking
| Property | Type | Default | Description |
| --------- | --------- | --------- | ---------------------------------------------------------------------------------- |
| `stacked` | `boolean` | `false` | Stack values on top of other series sharing the same stack |
| `stack` | `string` | `"stack"` | Name of the stack. Use different names for multiple independent stacks |
### Axes
| Property | Type | Default | Description |
| -------- | -------- | ------- | ----------------------------------- |
| `xAxis` | `string` | `"x"` | Name of the horizontal axis to use |
| `yAxis` | `string` | `"y"` | Name of the vertical axis to use |
### Legend
| Property | Type | Default | Description |
| -------------- | -------------- | ---------- | ----------------------------------------------------------------------------- |
| `name` | `string` | | Name of the series as it will appear in the legend |
| `active` | `boolean` | `true` | Indicates if the series is active. Inactive series are shown only in legend |
| `legend` | `string/false` | `"legend"` | Name of the legend to use. Set to `false` to hide from legend |
| `legendAction` | `string` | `"auto"` | Action to perform on legend item click |
| `legendShape` | `string` | `"rect"` | Shape to display in the legend entry |
---
# BarGraph
```ts
import { BarGraph } from 'cx/charts';
```
The `BarGraph` component renders a series of horizontal bars. Use with `CategoryAxis` on the Y axis for category labels.
For multiple series, use `size` and `offset` to position bars side by side. Click legend entries to toggle series visibility.
```tsx
import { Svg, Rectangle } from "cx/svg";
import {
Chart,
NumericAxis,
CategoryAxis,
Gridlines,
BarGraph,
Legend,
LegendScope,
} from "cx/charts";
import { createModel } from "cx/data";
import { Controller } from "cx/ui";
interface Model {
data: { city: string; q1: number; q2: number }[];
q1Active: boolean;
q2Active: boolean;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.data, [
{ city: "New York", q1: 85, q2: 92 },
{ city: "London", q1: 72, q2: 78 },
{ city: "Paris", q1: 65, q2: 71 },
{ city: "Tokyo", q1: 90, q2: 88 },
{ city: "Sydney", q1: 58, q2: 64 },
]);
this.store.set(m.q1Active, true);
this.store.set(m.q2Active, true);
}
}
export default (
);
```
## Configuration
| Property | Type | Description |
| -------------- | ---------- | ------------------------------------------------------------------ |
| `data` | `array` | Array of data points. |
| `xField` | `string` | Field name for the bar value (length). Default is `"x"`. |
| `yField` | `string` | Field name for the category. Default is `"y"`. |
| `colorIndex` | `number` | Index of the color from the theme palette. |
| `colorMap` | `string` | Name of the color map for automatic color assignment. |
| `name` | `string` | Series name for the legend. |
| `active` | `boolean` | Binding to control visibility. Works with Legend interaction. |
| `size` | `number` | Bar thickness as a fraction of available space. Default is `0.5`. |
| `offset` | `number` | Offset for positioning multiple series. Default is `0`. |
| `stacked` | `boolean` | Set to `true` to stack bars. |
| `borderRadius` | `number` | Corner radius for rounded bars. |
| `x0Field` | `string` | Field name for the bar start value (for range bars). |
| `selection` | `object` | Selection configuration for interactive bars. |
---
# Bar
```ts
import { Bar } from 'cx/charts';
```
The `Bar` component renders individual horizontal bars. Unlike `BarGraph` which takes data arrays, `Bar` is used with `Repeater` for more control over each bar.
Use `height` and `offset` to position multiple series side by side. Hover over bars to see tooltips.
```tsx
import { Svg, Rectangle } from "cx/svg";
import {
Chart,
NumericAxis,
CategoryAxis,
Gridlines,
Bar,
Legend,
LegendScope,
} from "cx/charts";
import { createModel } from "cx/data";
import { Controller, Repeater, format } from "cx/ui";
import { enableTooltips } from "cx/widgets";
enableTooltips();
interface Point {
category: string;
v1: number;
v2: number;
}
interface Model {
data: Point[];
$point: Point;
v1Active: boolean;
v2Active: boolean;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.data, [
{ category: "Product A", v1: 120, v2: 80 },
{ category: "Product B", v1: 90, v2: 150 },
{ category: "Product C", v1: 180, v2: 100 },
{ category: "Product D", v1: 60, v2: 130 },
]);
this.store.set(m.v1Active, true);
this.store.set(m.v2Active, true);
}
}
export default (
);
```
## Configuration
| Property | Type | Description |
| -------------- | --------- | ------------------------------------------------------------------ |
| `x` | `number` | Bar value (length). |
| `x0` | `number` | Bar start value for range bars. Default is `0`. |
| `y` | `string` | Category value. |
| `colorIndex` | `number` | Index of the color from the theme palette. |
| `colorMap` | `string` | Name of the color map for automatic color assignment. |
| `name` | `string` | Series name for the legend. |
| `active` | `boolean` | Binding to control visibility. Works with Legend interaction. |
| `height` | `number` | Bar thickness as a fraction of available space. Default is `0.5`. |
| `offset` | `number` | Offset for positioning multiple series. Default is `0`. |
| `stacked` | `boolean` | Set to `true` to stack bars. |
| `borderRadius` | `number` | Corner radius for rounded bars. |
| `tooltip` | `object` | Tooltip configuration. Use `tpl` for template strings. |
---
# ColumnGraph
```ts
import { ColumnGraph } from 'cx/charts';
```
The `ColumnGraph` component renders a series of vertical bars. Use with `CategoryAxis` on the X axis for category labels.
For multiple series, use `size` and `offset` to position columns side by side. Click legend entries to toggle series visibility.
```tsx
import { Svg, Rectangle } from "cx/svg";
import {
Chart,
NumericAxis,
CategoryAxis,
Gridlines,
ColumnGraph,
Legend,
LegendScope,
} from "cx/charts";
import { createModel } from "cx/data";
import { Controller } from "cx/ui";
interface Model {
data: { month: string; q1: number; q2: number }[];
q1Active: boolean;
q2Active: boolean;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.data, [
{ month: "Jan", q1: 42, q2: 38 },
{ month: "Feb", q1: 58, q2: 52 },
{ month: "Mar", q1: 65, q2: 71 },
{ month: "Apr", q1: 71, q2: 68 },
{ month: "May", q1: 85, q2: 79 },
{ month: "Jun", q1: 78, q2: 84 },
]);
this.store.set(m.q1Active, true);
this.store.set(m.q2Active, true);
}
}
export default (
);
```
## Configuration
| Property | Type | Description |
| -------------- | ---------- | -------------------------------------------------------------------- |
| `data` | `array` | Array of data points. |
| `xField` | `string` | Field name for the category. Default is `"x"`. |
| `yField` | `string` | Field name for the column value (height). Default is `"y"`. |
| `colorIndex` | `number` | Index of the color from the theme palette. |
| `colorMap` | `string` | Name of the color map for automatic color assignment. |
| `name` | `string` | Series name for the legend. |
| `active` | `boolean` | Binding to control visibility. Works with Legend interaction. |
| `size` | `number` | Column width as a fraction of available space. Default is `0.5`. |
| `offset` | `number` | Offset for positioning multiple series. Default is `0`. |
| `stacked` | `boolean` | Set to `true` to stack columns. |
| `borderRadius` | `number` | Corner radius for rounded columns. |
| `y0Field` | `string` | Field name for the column start value (for range columns). |
| `selection` | `object` | Selection configuration for interactive columns. |
---
# Column
```ts
import { Column } from 'cx/charts';
```
The `Column` component renders individual vertical bars. Unlike `ColumnGraph` which takes data arrays, `Column` is used with `Repeater` for more control over each column—such as individual colors based on value.
In this example, each column's color is computed from its value using `expr`.
```tsx
import { Svg, Rectangle } from "cx/svg";
import {
Chart,
NumericAxis,
CategoryAxis,
Gridlines,
Column,
LegendScope,
} from "cx/charts";
import { createModel } from "cx/data";
import { Controller, Repeater, expr, format } from "cx/ui";
import { enableTooltips } from "cx/widgets";
enableTooltips();
interface Point {
city: string;
value: number;
}
interface Model {
data: Point[];
$point: Point;
}
const m = createModel();
const cities = [
"Tokyo",
"Delhi",
"Shanghai",
"São Paulo",
"Mexico City",
"Cairo",
"Mumbai",
"Beijing",
"Dhaka",
"Osaka",
"New York",
"Karachi",
"Buenos Aires",
"Istanbul",
"Kolkata",
];
class PageController extends Controller {
onInit() {
this.store.set(
m.data,
cities.map((city, i) => ({
city,
value: 10 + ((i + 1) / 15) * 40 + (Math.random() - 0.5) * 10,
})),
);
}
}
export default (
);
```
## Configuration
| Property | Type | Description |
| -------------- | --------- | -------------------------------------------------------------------- |
| `x` | `string` | Category value. |
| `y` | `number` | Column value (height). |
| `y0` | `number` | Column start value for range columns. Default is `0`. |
| `colorIndex` | `number` | Index of the color from the theme palette. |
| `colorMap` | `string` | Name of the color map for automatic color assignment. |
| `name` | `string` | Series name for the legend. |
| `active` | `boolean` | Binding to control visibility. Works with Legend interaction. |
| `width` | `number` | Column width as a fraction of available space. Default is `0.5`. |
| `offset` | `number` | Offset for positioning multiple series. Default is `0`. |
| `stacked` | `boolean` | Set to `true` to stack columns. |
| `borderRadius` | `number` | Corner radius for rounded columns. |
| `tooltip` | `object` | Tooltip configuration. |
---
# PieChart
```ts
import { PieChart, PieSlice } from 'cx/charts';
```
Pie charts compare parts to the whole. Use `PieChart` as a container and `PieSlice` for each segment. The `ColorMap` component assigns unique colors to each slice. Click the legend to toggle slice visibility.
```tsx
import { Svg, Text, Rectangle, Line } from "cx/svg";
import { PieChart, PieSlice, Legend, ColorMap } from "cx/charts";
import { createModel } from "cx/data";
import { Controller, Repeater, LabelsTopLayout, KeySelection } from "cx/ui";
import { enableTooltips, Slider } from "cx/widgets";
enableTooltips();
interface Slice {
id: number;
name: string;
value: number;
active: boolean;
}
interface Model {
data: Slice[];
$record: Slice;
$index: number;
selection: number | null;
angle: number;
gap: number;
r0: number;
borderRadius: number;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.angle, 360);
this.store.set(m.gap, 4);
this.store.set(m.r0, 50);
this.store.set(m.borderRadius, 5);
this.store.set(m.data, [
{ id: 0, name: "Electronics", value: 35, active: true },
{ id: 1, name: "Clothing", value: 25, active: true },
{ id: 2, name: "Food", value: 20, active: true },
{ id: 3, name: "Books", value: 12, active: true },
{ id: 4, name: "Other", value: 8, active: true },
]);
}
}
export default (
);
```
## Configuration
### PieChart
| Property | Type | Default | Description |
| ------------ | --------- | ------- | -------------------------------------- |
| `angle` | `number` | `360` | Total angle of the pie in degrees. |
| `startAngle` | `number` | `0` | Starting angle in degrees. |
| `clockwise` | `boolean` | `false` | Set to `true` for clockwise direction. |
| `gap` | `number` | `0` | Gap between slices in pixels. |
### PieSlice
| Property | Type | Default | Description |
| ------------------- | ----------- | --------- | ------------------------------------------------------------------ |
| `value` | `number` | | Value represented by the slice. |
| `active` | `boolean` | `true` | Controls visibility. Bind to legend for toggle. |
| `r` | `number` | `50` | Outer radius (percentage of available space). |
| `r0` | `number` | `0` | Inner radius for donut charts. |
| `offset` | `number` | `0` | Offset from center to explode the slice. |
| `colorIndex` | `number` | | Color palette index (0-15). |
| `colorMap` | `string` | | Named color map for automatic colors. |
| `colorName` | `string` | | Name used to resolve color. Defaults to `name`. |
| `borderRadius` | `number` | `0` | Corner rounding in pixels. |
| `name` | `string` | | Name shown in legend. |
| `legend` | `string` | `legend` | Legend name. Set to `false` to hide. |
| `legendDisplayText` | `string` | | Custom text for legend entry. |
| `legendShape` | `string` | `circle` | Shape used in legend. |
| `legendAction` | `string` | `auto` | Action on legend click: `auto`, `toggle`, or `select`. |
| `stack` | `string` | `stack` | Stack name for multi-level pies. |
| `disabled` | `boolean` | `false` | Disables interaction with the slice. |
| `innerPointRadius` | `number` | | Inner radius for label line calculations. |
| `outerPointRadius` | `number` | | Outer radius for label line calculations. |
| `percentageRadius` | `boolean` | `true` | If `true`, `r` and `r0` are percentages of available space. |
| `hoverId` | `string` | | ID for hover synchronization. |
| `hoverChannel` | `string` | `default` | Channel for hover synchronization. |
| `selection` | `Selection` | | Selection configuration for click handling. |
| `tooltip` | `object` | | Tooltip configuration. |
---
# Marker
```ts
import { Marker } from 'cx/charts';
```
`Marker` displays individual data points on a chart. Markers are commonly used for scatter charts and can be made draggable for interactive data manipulation. Click on legend entries to toggle visibility of each dataset.
```tsx
import { Svg } from "cx/svg";
import { Chart, NumericAxis, Gridlines, Marker, Legend } from "cx/charts";
import { createModel } from "cx/data";
import { Repeater, Controller } from "cx/ui";
interface Point {
x: number;
y: number;
size: number;
color: number;
}
interface Model {
reds: Point[];
blues: Point[];
showReds: boolean;
showBlues: boolean;
$point: Point;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.showReds, true);
this.store.set(m.showBlues, true);
this.store.set(
m.reds,
Array.from({ length: 50 }, () => ({
x: 100 + Math.random() * 300,
y: Math.random() * 300,
size: 10 + Math.random() * 30,
color: Math.floor(Math.random() * 3),
})),
);
this.store.set(
m.blues,
Array.from({ length: 50 }, () => ({
x: Math.random() * 300,
y: 100 + Math.random() * 300,
size: 10 + Math.random() * 30,
color: 4 + Math.floor(Math.random() * 3),
})),
);
}
}
export default (
);
```
## Features
- Multiple shapes: circle, square, rect, triangle
- Variable size and color based on data
- Draggable in X and/or Y direction with axis constraints
- Tooltips on hover
- Legend integration with visibility toggle
## Configuration
| Property | Type | Default | Description |
| -------------- | --------- | ----------- | ------------------------------------------------------ |
| `x` | `number` | | X-axis position. |
| `y` | `number` | | Y-axis position. |
| `size` | `number` | `5` | Marker size in pixels. |
| `shape` | `string` | `"circle"` | Shape: `"circle"`, `"square"`, `"triangle"`. |
| `colorIndex` | `number` | | Color palette index. |
| `colorMap` | `string` | | Name of the color map to use. |
| `colorName` | `string` | | Name for color map lookup. |
| `name` | `string` | | Name for legend. Also used as `colorName` if not set. |
| `legend` | `string` | `"legend"` | Legend name to show in. |
| `active` | `boolean` | `true` | Whether the marker is active (visible). |
| `draggableX` | `boolean` | `false` | Enable horizontal dragging. |
| `draggableY` | `boolean` | `false` | Enable vertical dragging. |
| `constrain` | `boolean` | `true` | Constrain dragging to chart bounds. |
| `tooltip` | `object` | | Tooltip configuration. |
| `affectsAxes` | `boolean` | `true` | Whether marker position affects axis range calculation.|
---
# ScatterGraph
```ts
import { ScatterGraph } from 'cx/charts';
```
`ScatterGraph` renders scatter plots from an array of data points. Unlike `Marker` which renders individual points, `ScatterGraph` efficiently renders entire datasets and is better suited for visualizing large amounts of data.
```tsx
import { Svg } from "cx/svg";
import { Chart, NumericAxis, Gridlines, ScatterGraph, Legend } from "cx/charts";
import { createModel } from "cx/data";
import { Controller } from "cx/ui";
interface Point {
x: number;
y: number;
size: number;
}
interface Model {
reds: Point[];
blues: Point[];
showReds: boolean;
showBlues: boolean;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.showReds, true);
this.store.set(m.showBlues, true);
this.store.set(
m.reds,
Array.from({ length: 200 }, () => ({
x: 100 + Math.random() * 300,
y: Math.random() * 300,
size: Math.random() * 20,
})),
);
this.store.set(
m.blues,
Array.from({ length: 200 }, () => ({
x: Math.random() * 300,
y: 100 + Math.random() * 300,
size: Math.random() * 20,
})),
);
}
}
export default (
);
```
## Features
- Efficient rendering of large datasets
- Variable point size based on data field
- Multiple shapes: circle, square, triangle
- Legend integration with visibility toggle
## Configuration
| Property | Type | Default | Description |
| ------------ | --------- | ---------- | ----------------------------------------- |
| `data` | `array` | | Array of data points. |
| `xField` | `string` | `"x"` | Field name for x-axis values. |
| `yField` | `string` | `"y"` | Field name for y-axis values. |
| `sizeField` | `string` | | Field name for point size. |
| `size` | `number` | `5` | Default point size when sizeField not set.|
| `shape` | `string` | `"circle"` | Shape: `"circle"`, `"square"`, `"triangle"`.|
| `colorIndex` | `number` | | Color palette index. |
| `colorMap` | `string` | | Name of the color map to use. |
| `colorName` | `string` | | Name for color map lookup. |
| `name` | `string` | | Name for legend. |
| `legend` | `string` | `"legend"` | Legend name to show in. |
| `active` | `boolean` | `true` | Whether the graph is active (visible). |
---
# MarkerLine
```ts
import { MarkerLine } from 'cx/charts';
```
`MarkerLine` draws horizontal or vertical lines to highlight important values such as minimum, maximum, thresholds, or averages. Child elements (like `Text`) can be placed inside to add labels.
```tsx
import { Svg, Text } from "cx/svg";
import {
Chart,
NumericAxis,
Gridlines,
LineGraph,
MarkerLine,
Legend,
LegendScope,
} from "cx/charts";
import { createModel } from "cx/data";
import { Controller } from "cx/ui";
import { Button } from "cx/widgets";
interface Point {
x: number;
y: number;
}
interface Extremes {
min: number;
max: number;
}
interface Model {
points: Point[];
extremes: Extremes;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.generate();
this.addComputable(m.extremes, [m.points], (points) => {
if (!points || points.length === 0) return { min: 0, max: 0 };
let min = points[0].y;
let max = points[0].y;
for (let i = 1; i < points.length; i++) {
if (points[i].y < min) min = points[i].y;
if (points[i].y > max) max = points[i].y;
}
return { min, max };
});
}
generate() {
let y = 100;
this.store.set(
m.points,
Array.from({ length: 101 }, (_, i) => ({
x: i * 4,
y: (y = y + (Math.random() - 0.5) * 30),
})),
);
}
}
export default (
Generate
);
```
> Note how `deadZone` is used on the vertical axis to reserve space for min/max labels.
## Configuration
| Property | Type | Default | Description |
| -------------- | --------- | ---------- | ------------------------------------------------- |
| `x` | `number` | | Draw a vertical line at this x value. |
| `y` | `number` | | Draw a horizontal line at this y value. |
| `colorIndex` | `number` | | Color palette index. |
| `colorMap` | `string` | | Name of the color map to use. |
| `colorName` | `string` | | Name for color map lookup. |
| `name` | `string` | | Name for legend. |
| `legend` | `string` | `"legend"` | Legend name to show in. |
| `active` | `boolean` | `true` | Whether the line is active (visible). |
| `affectsAxes` | `boolean` | `true` | Whether line position affects axis range. |
| `lineStyle` | `string` | | CSS style string for the line. |
| `class` | `string` | | CSS class for the line element. |
---
# ColorMap
```ts
import { ColorMap } from 'cx/charts';
```
`ColorMap` automatically assigns colors from the palette to chart elements. Elements register themselves by name and receive consistent color indices. This is useful when the number of series is dynamic.
```tsx
import { Svg } from "cx/svg";
import {
Chart,
NumericAxis,
Gridlines,
LineGraph,
ColorMap,
Legend,
} from "cx/charts";
import { createModel } from "cx/data";
import { Repeater, Controller } from "cx/ui";
interface Point {
x: number;
y: number;
}
interface Series {
name: string;
active: boolean;
points: Point[];
}
interface Model {
series: Series[];
$record: Series;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(
m.series,
Array.from({ length: 5 }, (_, i) => {
let y = 50 + Math.random() * 50;
return {
name: `Series ${i + 1}`,
active: true,
points: Array.from({ length: 20 }, (_, x) => ({
x: x * 5,
y: (y = y + Math.random() * 20 - 10),
})),
};
}),
);
}
}
export default (
);
```
## How It Works
1. Place `ColorMap` above chart elements that need automatic colors
2. Elements use `colorMap="mapName"` to register with a specific color map
3. ColorMap assigns colors based on element names, distributing them evenly across the palette
4. Use `ColorMap.Scope` to isolate multiple color maps in nested charts
## Configuration
| Property | Type | Default | Description |
| -------- | ---------- | ------- | -------------------------------------------------------- |
| `offset` | `number` | `0` | Starting offset in the color palette. |
| `step` | `number` | | Step between colors. Auto-calculated if not specified. |
| `size` | `number` | `16` | Size of the color palette. |
| `names` | `string[]` | | Pre-register names to ensure consistent color assignment.|
---
# updateArray
```ts
import { updateArray } from 'cx/data';
```
`updateArray` performs immutable updates on arrays. It applies a callback to transform items, optionally filtering which items to update and which to remove. Returns the original array if no changes were made.
```tsx
import { updateArray, createModel } from "cx/data";
import { Controller } from "cx/ui";
import { Button, Grid } from "cx/widgets";
interface Item {
id: number;
name: string;
count: number;
}
interface Model {
items: Item[];
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.items, [
{ id: 1, name: "Apple", count: 5 },
{ id: 2, name: "Banana", count: 3 },
{ id: 3, name: "Cherry", count: 8 },
{ id: 4, name: "Date", count: 2 },
]);
}
incrementAll() {
this.store.update(m.items, (items) =>
updateArray(items, (item) => ({ ...item, count: item.count + 1 })),
);
}
incrementEven() {
this.store.update(m.items, (items) =>
updateArray(
items,
(item) => ({ ...item, count: item.count + 1 }),
(item) => item.id % 2 === 0,
),
);
}
removeSmall() {
this.store.update(m.items, (items) =>
updateArray(
items,
(item) => item,
null,
(item) => item.count < 3,
),
);
}
reset() {
this.onInit();
}
}
export default (
Increment AllIncrement Even IDsRemove Count < 3Reset
);
```
## Signature
```ts
function updateArray(
array: T[],
updateCallback: (item: T, index: number) => T,
itemFilter?: (item: T, index: number) => boolean,
removeFilter?: (item: T, index: number) => boolean
): T[]
```
## Parameters
| Parameter | Type | Description |
| ---------------- | ---------- | -------------------------------------------------------------- |
| `array` | `T[]` | The array to update. |
| `updateCallback` | `function` | Transform function applied to each item (or filtered items). |
| `itemFilter` | `function` | Optional. If provided, only items matching this filter are updated. |
| `removeFilter` | `function` | Optional. Items matching this filter are removed from the result. |
## Return Value
Returns a new array with updates applied, or the original array if nothing changed (preserves reference equality).
## Examples
### Update all items
```ts
const items = [{ id: 1, count: 5 }, { id: 2, count: 3 }];
const updated = updateArray(items, (item) => ({
...item,
count: item.count + 1,
}));
// [{ id: 1, count: 6 }, { id: 2, count: 4 }]
```
### Update specific items
```ts
const items = [{ id: 1, count: 5 }, { id: 2, count: 3 }];
const updated = updateArray(
items,
(item) => ({ ...item, count: item.count * 2 }),
(item) => item.id === 2, // only update item with id 2
);
// [{ id: 1, count: 5 }, { id: 2, count: 6 }]
```
### Remove items while updating
```ts
const items = [{ id: 1, count: 5 }, { id: 2, count: 3 }];
const updated = updateArray(
items,
(item) => item,
null, // no filter for updates
(item) => item.count < 4, // remove items with count < 4
);
// [{ id: 1, count: 5 }]
```
## See Also
- [filter](/docs/utilities/filter) - Immutable array filtering
- [append](/docs/utilities/append) - Immutable array append
---
# append
```ts
import { append } from 'cx/data';
```
`append` creates a new array with items added to the end. Handles null/undefined arrays gracefully.
```tsx
import { append, createModel } from "cx/data";
import { Controller } from "cx/ui";
import { Button, Grid, TextField } from "cx/widgets";
interface Item {
id: number;
name: string;
}
interface Model {
items: Item[];
newName: string;
nextId: number;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.items, [
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
]);
this.store.set(m.nextId, 3);
}
addItem() {
const name = this.store.get(m.newName);
if (!name) return;
const id = this.store.get(m.nextId);
this.store.update(m.items, (items) => append(items, { id, name }));
this.store.set(m.nextId, id + 1);
this.store.set(m.newName, "");
}
addMultiple() {
const id = this.store.get(m.nextId);
this.store.update(m.items, (items) =>
append(items, { id, name: "Item A" }, { id: id + 1, name: "Item B" }),
);
this.store.set(m.nextId, id + 2);
}
reset() {
this.onInit();
}
}
export default (
Add ItemAdd MultipleReset
);
```
## Signature
```ts
function append(array: T[], ...items: T[]): T[]
```
## Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------ |
| `array` | `T[]` | The source array (can be null/undefined). |
| `items` | `T[]` | Items to append. |
## Return Value
Returns a new array with items appended. If `array` is null/undefined, returns the items as a new array.
## Examples
### Basic usage
```ts
const items = [1, 2, 3];
const result = append(items, 4, 5);
// [1, 2, 3, 4, 5]
// Original array is unchanged
console.log(items); // [1, 2, 3]
```
### With null array
```ts
const result = append(null, 1, 2);
// [1, 2]
```
### No items to append
```ts
const items = [1, 2, 3];
const result = append(items);
// [1, 2, 3] - returns empty array if source is null, otherwise same reference
```
## See Also
- [insertElement](/docs/utilities/insert-element) - Insert at specific position
- [updateArray](/docs/utilities/update-array) - Update items in array
---
# filter
```ts
import { filter } from 'cx/data';
```
`filter` performs immutable array filtering. Unlike native `Array.filter()`, it returns the original array reference if no items are removed, enabling efficient change detection.
```tsx
import { filter, createModel } from "cx/data";
import { Controller } from "cx/ui";
import { Button, Grid, NumberField } from "cx/widgets";
interface Item {
id: number;
name: string;
value: number;
}
interface Model {
items: Item[];
threshold: number;
}
const m = createModel();
class PageController extends Controller {
onInit() {
this.store.set(m.items, [
{ id: 1, name: "Alpha", value: 10 },
{ id: 2, name: "Beta", value: 25 },
{ id: 3, name: "Gamma", value: 5 },
{ id: 4, name: "Delta", value: 30 },
{ id: 5, name: "Epsilon", value: 15 },
]);
this.store.set(m.threshold, 15);
}
filterAboveThreshold() {
const threshold = this.store.get(m.threshold);
this.store.update(m.items, (items) =>
filter(items, (item) => item.value >= threshold),
);
}
filterEvenIds() {
this.store.update(m.items, (items) =>
filter(items, (item) => item.id % 2 === 0),
);
}
reset() {
this.onInit();
}
}
export default (
Threshold:Keep ≥ ThresholdKeep Even IDsReset
);
```
## Signature
```ts
function filter(
array: T[],
callback: (item: T, index: number, array: T[]) => boolean,
): T[];
```
## Parameters
| Parameter | Type | Description |
| ---------- | ---------- | ----------------------------------------------- |
| `array` | `T[]` | The array to filter. |
| `callback` | `function` | Predicate function. Return `true` to keep item. |
## Return Value
Returns a new array with items that pass the test, or the **original array** if all items pass (preserves reference equality).
## Examples
### Basic usage
```ts
const items = [1, 2, 3, 4, 5];
const result = filter(items, (n) => n > 2);
// [3, 4, 5]
```
### Reference preservation
```ts
const items = [1, 2, 3];
// All items pass - returns same reference
const result1 = filter(items, (n) => n > 0);
console.log(result1 === items); // true
// Some items removed - returns new array
const result2 = filter(items, (n) => n > 1);
console.log(result2 === items); // false
```
### With null array
```ts
const result = filter(null, (n) => n > 0);
// null
```
## Why Use This Instead of Array.filter()?
The native `Array.filter()` always returns a new array, even if no items are removed. This can cause unnecessary re-renders in reactive frameworks since reference equality checks fail.
```ts
const items = [1, 2, 3];
// Native filter - always new array
const native = items.filter((n) => n > 0);
console.log(native === items); // false (always)
// cx filter - preserves reference if unchanged
const cx = filter(items, (n) => n > 0);
console.log(cx === items); // true
```
## See Also
- [updateArray](/docs/utilities/update-array) - Update and optionally remove items
- [append](/docs/utilities/append) - Add items to array
---
# updateTree
```ts
import { updateTree } from 'cx/data';
```
Recursively updates nodes in a tree structure that match a filter.
Returns a new tree with updated nodes, or the original tree if no changes were made.
```tsx
const updated = updateTree(
array,
updateCallback,
itemFilter,
childrenField,
removeFilter,
);
```
| Argument | Type | Description |
| ---------------- | ------------------------------ | --------------------------------------------------------------------------------- |
| `array` | `T[]` | Tree data array to update. |
| `updateCallback` | `(node: T) => T` | Function that receives a node and returns the updated node. |
| `itemFilter` | `(node: T) => boolean \| null` | Predicate returning `true` for nodes to update. If `null`, all nodes are updated. |
| `childrenField` | `keyof T` | Name of the property containing child nodes. |
| `removeFilter` | `(node: T) => boolean` | Optional predicate returning `true` for nodes to remove. |
**Returns:** A new tree with updates applied, or the original array if unchanged.
## Examples
### Expand all folders
```tsx
updateTree(
data,
(node) => ({ ...node, $expanded: true }),
(node) => !node.$leaf,
"$children",
);
```
### Collapse all folders
```tsx
updateTree(
data,
(node) => ({ ...node, $expanded: false }),
(node) => !node.$leaf,
"$children",
);
```
### Rename a node
```tsx
updateTree(
data,
(node) => ({ ...node, name: newName }),
(node) => node.id === targetId,
"$children",
);
```
### Add child to a node
```tsx
updateTree(
data,
(node) => ({
...node,
$expanded: true,
$children: [...(node.$children || []), newChild],
}),
(node) => node.id === parentId,
"$children",
);
```
### Update all nodes
Pass `null` as the filter to update every node in the tree:
```tsx
updateTree(data, (node) => ({ ...node, visited: true }), null, "$children");
```
### Update and remove in one pass
Use `removeFilter` to remove nodes while updating others:
```tsx
updateTree(
data,
(node) => ({ ...node, $expanded: true }),
(node) => !node.$leaf,
"$children",
(node) => node.deleted, // Remove nodes marked as deleted
);
```
See also: [findTreeNode](/docs/utilities/find-tree-node), [removeTreeNodes](/docs/utilities/remove-tree-nodes), [Tree Operations](/docs/tables/tree-operations)
---
# findTreeNode
```ts
import { findTreeNode } from 'cx/data';
```
Recursively searches a tree structure for a node matching the criteria.
```tsx
const node = findTreeNode(array, criteria, childrenField);
```
| Argument | Type | Description |
| -------- | ---- | ----------- |
| `array` | `T[]` | Tree data array to search. |
| `criteria` | `(node: T) => boolean` | Predicate returning `true` when a matching node is found. |
| `childrenField` | `keyof T` | Name of the property containing child nodes. |
**Returns:** The first matching node, or `false` if not found.
## Examples
### Find node by ID
```tsx
const node = findTreeNode(
data,
(node) => node.id === targetId,
"$children"
);
```
### Check if node is a leaf
```tsx
const node = findTreeNode(data, (n) => n.id === selectedId, "$children");
if (node && node.$leaf) {
// Cannot add children to a leaf node
}
```
### Find by name
```tsx
const node = findTreeNode(
data,
(node) => node.name === "Documents",
"$children"
);
```
### Validate before operation
Use `findTreeNode` to validate a node before performing operations:
```tsx
function addChild(parentId: number, newChild: TreeNode) {
const parent = findTreeNode(data, (n) => n.id === parentId, "$children");
if (!parent) {
alert("Parent not found");
return;
}
if (parent.$leaf) {
alert("Cannot add children to a file");
return;
}
// Proceed with adding the child...
}
```
See also: [updateTree](/docs/utilities/update-tree), [removeTreeNodes](/docs/utilities/remove-tree-nodes), [Tree Operations](/docs/tables/tree-operations)
---
# removeTreeNodes
```ts
import { removeTreeNodes } from 'cx/data';
```
Recursively removes nodes from a tree structure that match the criteria.
Returns a new tree without the matching nodes, or the original tree if no nodes were removed.
```tsx
const filtered = removeTreeNodes(array, criteria, childrenField);
```
| Argument | Type | Description |
| -------- | ---- | ----------- |
| `array` | `T[]` | Tree data array to filter. |
| `criteria` | `(node: T) => boolean` | Predicate returning `true` for nodes to remove. |
| `childrenField` | `keyof T` | Name of the property containing child nodes. |
**Returns:** A new tree with matching nodes removed, or the original array if unchanged.
## Examples
### Delete node by ID
```tsx
removeTreeNodes(
data,
(node) => node.id === targetId,
"$children"
);
```
### Remove all leaf nodes
```tsx
removeTreeNodes(
data,
(node) => node.$leaf === true,
"$children"
);
```
### Remove empty folders
```tsx
removeTreeNodes(
data,
(node) => !node.$leaf && (!node.$children || node.$children.length === 0),
"$children"
);
```
### Remove with confirmation
```tsx
function deleteNode(nodeId: number) {
const node = findTreeNode(data, (n) => n.id === nodeId, "$children");
if (!node) return;
if (!node.$leaf && node.$children?.length > 0) {
if (!confirm("Delete folder and all its contents?")) {
return;
}
}
store.update(m.data, (data) =>
removeTreeNodes(data, (n) => n.id === nodeId, "$children")
);
}
```
See also: [updateTree](/docs/utilities/update-tree), [findTreeNode](/docs/utilities/find-tree-node), [Tree Operations](/docs/tables/tree-operations)
---
# getSearchQueryPredicate
```ts
import { getSearchQueryPredicate } from 'cx/util';
```
The `getSearchQueryPredicate` function creates a predicate function that tests if a text matches a search query.
It supports multi-word queries where all terms must match.
## Basic Usage
```tsx
import { getSearchQueryPredicate } from "cx/util";
const predicate = getSearchQueryPredicate("john doe");
predicate("John Doe Smith"); // true - contains both "john" and "doe"
predicate("John Smith"); // false - missing "doe"
predicate("Jane Doe"); // false - missing "john"
```
## How It Works
The function splits the query into individual words and creates case-insensitive regular expressions for each term.
A text matches only if it contains all search terms.
```tsx
// Empty query matches everything
const matchAll = getSearchQueryPredicate("");
matchAll("anything"); // true
// Single term
const single = getSearchQueryPredicate("react");
single("React Components"); // true
// Multiple terms - all must match
const multi = getSearchQueryPredicate("react hook");
multi("React Custom Hook"); // true
multi("React Components"); // false - missing "hook"
```
## Filtering Lists
The predicate is commonly used to filter arrays of records.
```tsx
import { getSearchQueryPredicate } from "cx/util";
interface User {
name: string;
email: string;
}
const users: User[] = [
{ name: "John Doe", email: "john@example.com" },
{ name: "Jane Smith", email: "jane@example.com" },
{ name: "Bob Johnson", email: "bob@example.com" },
];
const query = "john";
const predicate = getSearchQueryPredicate(query);
// Filter by name
const filtered = users.filter((user) => predicate(user.name));
// Result: [{ name: "John Doe", ... }, { name: "Bob Johnson", ... }]
// Filter by multiple fields
const multiFieldFiltered = users.filter(
(user) => predicate(user.name) || predicate(user.email)
);
```
## API
```tsx
function getSearchQueryPredicate(
query: string,
options?: any
): (text: string) => boolean;
```
| Parameter | Type | Description |
| --- | --- | --- |
| query | `string` | The search query string |
| options | `any` | Reserved for future use |
**Returns:** A predicate function that tests if text matches all query terms.
## See Also
- [getSearchQueryHighlighter](/docs/utilities/get-search-query-highlighter) - Highlight matched terms in text
- [HighlightedSearchText](/docs/forms/highlighted-search-text) - Built-in component for highlighting
---
# isArray
```ts
import { isArray } from 'cx/util';
```
Type guard that checks if a value is an array.
## Signature
```ts
function isArray(value: any): value is any[]
```
## Examples
```ts
isArray([1, 2, 3]); // true
isArray([]); // true
isArray("hello"); // false
isArray({ length: 3 }); // false
isArray(null); // false
isArray(undefined); // false
```
## Use Cases
```ts
function processItems(input: unknown) {
if (isArray(input)) {
// TypeScript knows input is any[] here
return input.map(item => process(item));
}
return [process(input)];
}
```
## See Also
- [isNonEmptyArray](/docs/utilities/is-non-empty-array) - Check for non-empty array
- [isObject](/docs/utilities/is-object) - Check for object
---
# isString
```ts
import { isString } from 'cx/util';
```
The `isString` type guard checks if a value is a string.
## Basic Usage
```tsx
import { isString } from "cx/util";
isString("hello"); // true
isString(""); // true
isString(`template`); // true
isString(42); // false
isString(null); // false
isString(undefined); // false
isString(["a", "b"]); // false
isString(new String("hello")); // false (String object, not primitive)
```
## Type Narrowing
The function is a TypeScript type guard that narrows the type to `string`.
```tsx
import { isString } from "cx/util";
function formatValue(value: unknown): string {
if (isString(value)) {
// value is typed as 'string' here
return value.toUpperCase();
}
return String(value);
}
```
## Common Use Cases
### Optional String Parameters
```tsx
import { isString } from "cx/util";
interface Options {
label?: string | (() => string);
}
function getLabel(options: Options): string {
const { label } = options;
if (isString(label)) {
return label;
}
if (typeof label === "function") {
return label();
}
return "Default Label";
}
```
### Safe String Operations
```tsx
import { isString } from "cx/util";
function trimIfString(value: unknown): unknown {
return isString(value) ? value.trim() : value;
}
function toLowerCase(value: unknown): string | null {
return isString(value) ? value.toLowerCase() : null;
}
```
### Form Input Processing
```tsx
import { isString } from "cx/util";
function processFormValue(value: unknown): string {
if (!isString(value)) {
throw new Error("Expected string value");
}
return value.trim();
}
```
## API
```tsx
function isString(x: unknown): x is string;
```
| Parameter | Type | Description |
| --- | --- | --- |
| x | `unknown` | The value to check |
**Returns:** `true` if the value is a primitive string.
---
# isNumber
```ts
import { isNumber } from 'cx/util';
```
The `isNumber` type guard checks if a value is a number (including `NaN` and `Infinity`).
## Basic Usage
```tsx
import { isNumber } from "cx/util";
isNumber(42); // true
isNumber(3.14); // true
isNumber(0); // true
isNumber(-1); // true
isNumber(NaN); // true (NaN is typeof "number")
isNumber(Infinity); // true
isNumber("42"); // false
isNumber(null); // false
isNumber(undefined); // false
isNumber(new Number(42)); // false (Number object, not primitive)
```
## Type Narrowing
The function is a TypeScript type guard that narrows the type to `number`.
```tsx
import { isNumber } from "cx/util";
function calculateTotal(values: unknown[]): number {
return values.reduce((sum: number, value) => {
if (isNumber(value)) {
return sum + value;
}
return sum;
}, 0);
}
```
## Common Use Cases
### Numeric Validation
```tsx
import { isNumber } from "cx/util";
function isValidNumber(value: unknown): value is number {
return isNumber(value) && !isNaN(value) && isFinite(value);
}
isValidNumber(42); // true
isValidNumber(NaN); // false
isValidNumber(Infinity); // false
```
### Default Values
```tsx
import { isNumber } from "cx/util";
function getNumericValue(value: unknown, defaultValue: number): number {
return isNumber(value) && !isNaN(value) ? value : defaultValue;
}
getNumericValue(42, 0); // 42
getNumericValue("42", 0); // 0
getNumericValue(undefined, 0); // 0
```
### Safe Math Operations
```tsx
import { isNumber } from "cx/util";
function safeAdd(a: unknown, b: unknown): number | null {
if (isNumber(a) && isNumber(b)) {
return a + b;
}
return null;
}
function formatNumber(value: unknown, decimals: number = 2): string {
if (isNumber(value) && !isNaN(value)) {
return value.toFixed(decimals);
}
return "N/A";
}
```
### Data Processing
```tsx
import { isNumber } from "cx/util";
interface DataPoint {
x: unknown;
y: unknown;
}
function filterValidPoints(data: DataPoint[]): { x: number; y: number }[] {
return data.filter(
(point): point is { x: number; y: number } =>
isNumber(point.x) && isNumber(point.y)
);
}
```
## API
```tsx
function isNumber(x: any): x is number;
```
| Parameter | Type | Description |
| --- | --- | --- |
| x | `any` | The value to check |
**Returns:** `true` if the value is a primitive number (including `NaN` and `Infinity`).
---
# isFunction
```ts
import { isFunction } from 'cx/util';
```
The `isFunction` type guard checks if a value is a function.
## Basic Usage
```tsx
import { isFunction } from "cx/util";
isFunction(() => {}); // true
isFunction(function () {}); // true
isFunction(Math.max); // true
isFunction(Array.isArray); // true
isFunction(class Foo {}); // true (classes are functions)
isFunction(async () => {}); // true
isFunction("string"); // false
isFunction(42); // false
isFunction(null); // false
isFunction({}); // false
```
## Type Narrowing
The function is a TypeScript type guard that narrows the type to `Function`.
```tsx
import { isFunction } from "cx/util";
function invokeIfFunction(value: unknown): unknown {
if (isFunction(value)) {
// value is typed as 'Function' here
return value();
}
return value;
}
```
## Common Use Cases
### Callback Handling
```tsx
import { isFunction } from "cx/util";
interface Options {
onSuccess?: () => void;
onError?: (error: Error) => void;
}
function execute(options: Options): void {
try {
// ... do work
if (isFunction(options.onSuccess)) {
options.onSuccess();
}
} catch (error) {
if (isFunction(options.onError)) {
options.onError(error as Error);
}
}
}
```
### Dynamic Property Resolution
```tsx
import { isFunction } from "cx/util";
type ValueOrGetter = T | (() => T);
function resolveValue(valueOrGetter: ValueOrGetter): T {
return isFunction(valueOrGetter) ? valueOrGetter() : valueOrGetter;
}
resolveValue(42); // 42
resolveValue(() => 42); // 42
```
### Event Handler Validation
```tsx
import { isFunction } from "cx/util";
function addEventListener(
element: HTMLElement,
event: string,
handler: unknown
): void {
if (!isFunction(handler)) {
throw new Error(`Handler for "${event}" must be a function`);
}
element.addEventListener(event, handler as EventListener);
}
```
### Factory Pattern
```tsx
import { isFunction } from "cx/util";
type Factory = T | (() => T);
function createInstance(factory: Factory): T {
return isFunction(factory) ? factory() : factory;
}
const config = createInstance({ name: "app" });
const dynamicConfig = createInstance(() => ({ name: "dynamic-app" }));
```
## API
```tsx
function isFunction(f: any): f is Function;
```
| Parameter | Type | Description |
| --- | --- | --- |
| f | `any` | The value to check |
**Returns:** `true` if the value is a function (including arrow functions, methods, and classes).
---
# isDefined
```ts
import { isDefined } from 'cx/util';
```
Type guard that checks if a value is not `undefined`. Note: `null` is considered defined.
## Signature
```ts
function isDefined(value: T | undefined): value is T
```
## Examples
```ts
isDefined("hello"); // true
isDefined(0); // true
isDefined(false); // true
isDefined(null); // true (null is defined)
isDefined(undefined); // false
isDefined(void 0); // false
```
## Use Cases
```ts
function getConfig(options?: { timeout?: number }) {
if (isDefined(options?.timeout)) {
// Use the provided timeout
return { timeout: options.timeout };
}
// Use default
return { timeout: 5000 };
}
```
## See Also
- [coalesce](/docs/utilities/coalesce) - Return first non-null value
- [isUndefined](/docs/utilities/is-defined) - Opposite check
---
# isUndefined
```ts
import { isUndefined } from 'cx/util';
```
The `isUndefined` type guard checks if a value is strictly `undefined`.
## Basic Usage
```tsx
import { isUndefined } from "cx/util";
isUndefined(undefined); // true
isUndefined(void 0); // true
isUndefined(null); // false
isUndefined(""); // false
isUndefined(0); // false
isUndefined(false); // false
```
## Type Narrowing
The function is a TypeScript type guard that narrows the type to `undefined`.
```tsx
import { isUndefined } from "cx/util";
function process(value: string | undefined): string {
if (isUndefined(value)) {
return "default";
}
return value; // typed as string
}
```
## vs isDefined
`isUndefined` is the inverse of `isDefined`.
```tsx
import { isUndefined, isDefined } from "cx/util";
const value: string | undefined = getValue();
if (isUndefined(value)) {
// value is undefined
}
if (isDefined(value)) {
// value is string
}
```
## API
```tsx
function isUndefined(x: any): x is undefined;
```
| Parameter | Type | Description |
| --- | --- | --- |
| x | `any` | The value to check |
**Returns:** `true` if the value is strictly `undefined`.
## See Also
- [isDefined](/docs/utilities/is-defined) - Check if value is not undefined
---
# isNonEmptyArray
```ts
import { isNonEmptyArray } from 'cx/util';
```
The `isNonEmptyArray` type guard checks if a value is an array with at least one element.
## Basic Usage
```tsx
import { isNonEmptyArray } from "cx/util";
isNonEmptyArray([1, 2, 3]); // true
isNonEmptyArray(["a"]); // true
isNonEmptyArray([undefined]); // true (has one element)
isNonEmptyArray([]); // false
isNonEmptyArray(null); // false
isNonEmptyArray(undefined); // false
isNonEmptyArray("string"); // false
isNonEmptyArray({ length: 1 }); // false (not an array)
```
## Type Narrowing
The function is a TypeScript type guard that narrows the type to a non-empty tuple.
```tsx
import { isNonEmptyArray } from "cx/util";
function getFirst(items: T[]): T | undefined {
if (isNonEmptyArray(items)) {
// items is typed as [T, ...T[]] here
return items[0]; // Safe access, guaranteed to exist
}
return undefined;
}
```
## Common Use Cases
### Safe Array Operations
```tsx
import { isNonEmptyArray } from "cx/util";
function processItems(items: T[] | undefined, processor: (item: T) => void): void {
if (isNonEmptyArray(items)) {
items.forEach(processor);
}
}
function getFirstOrDefault(items: T[], defaultValue: T): T {
return isNonEmptyArray(items) ? items[0] : defaultValue;
}
function getLastOrDefault(items: T[], defaultValue: T): T {
return isNonEmptyArray(items) ? items[items.length - 1] : defaultValue;
}
```
### Conditional Rendering
```tsx
import { isNonEmptyArray } from "cx/util";
interface ListProps {
items: T[];
renderItem: (item: T) => JSX.Element;
emptyMessage?: string;
}
function List({ items, renderItem, emptyMessage }: ListProps) {
if (!isNonEmptyArray(items)) {
return
{emptyMessage || "No items"}
;
}
return (
{items.map(renderItem)}
);
}
```
### Data Validation
```tsx
import { isNonEmptyArray } from "cx/util";
interface FormData {
tags?: string[];
categories?: string[];
}
function validateForm(data: FormData): string[] {
const errors: string[] = [];
if (!isNonEmptyArray(data.tags)) {
errors.push("At least one tag is required");
}
if (!isNonEmptyArray(data.categories)) {
errors.push("At least one category is required");
}
return errors;
}
```
### Reduce with Initial Value
```tsx
import { isNonEmptyArray } from "cx/util";
function sum(numbers: number[]): number {
if (!isNonEmptyArray(numbers)) {
return 0;
}
return numbers.reduce((a, b) => a + b);
}
function average(numbers: number[]): number | null {
if (!isNonEmptyArray(numbers)) {
return null;
}
return numbers.reduce((a, b) => a + b) / numbers.length;
}
```
## API
```tsx
function isNonEmptyArray(x: any): x is [any, ...any];
```
| Parameter | Type | Description |
| --- | --- | --- |
| x | `any` | The value to check |
**Returns:** `true` if the value is an array with `length > 0`.