CxJS is a feature-rich JavaScript (TypeScript) framework for building complex web front-ends, such as portals, dashboards and admin applications. # 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 (
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 (
({ x, y: 75 - 50 * Math.random(), }))} /> ({ x, y: 75 - 50 * Math.random(), }))} /> ({ x, y: 75 - 50 * Math.random(), }))} />
); ``` The same `LineChart` component renders three different chart styles by passing different props. ## Conditional Logic Functional components can contain conditional logic: ```tsx import { createModel } from "cx/data"; import { createFunctionalComponent, LabelsLeftLayout, LabelsTopLayout, } from "cx/ui"; import { TextField } from "cx/widgets"; interface PageModel { form: { firstName: string; lastName: string; }; } const m = createModel(); interface MyFormProps { vertical?: boolean; } const MyForm = createFunctionalComponent(({ vertical }: MyFormProps) => { let layout = !vertical ? LabelsLeftLayout : { type: LabelsTopLayout, vertical: true }; return (
); }); export default (
); ``` The `vertical` prop changes the layout at render time. ## Reserved Properties These properties are handled by the framework and should not be used inside the function body: | Property | Type | Description | | ------------- | ------------ | ---------------------------------------------------------------------------------------- | | `visible` | `boolean` | If `false`, the component won't render and its controller won't initialize. Alias: `if`. | | `controller` | `Controller` | Controller that will be initialized with the component. | | `layout` | `Layout` | Inner layout applied to child elements. | | `outerLayout` | `Layout` | Outer layout that wraps the component. | | `putInto` | `string` | Content placeholder name for outer layouts. Alias: `contentFor`. | --- # Custom Components ```ts import { Widget } from 'cx/ui'; ``` CxJS includes many built-in components, but you can create custom ones when needed. Custom components extend base classes like `Widget`, `HtmlElement`, `Field`, or `PureContainer`. ## Example This example creates a `Square` component with bindable color properties. Click the square to set a random color. **Square.tsx** - The widget class (uses React JSX): **Usage** - CxJS application code: ```tsx import { createModel } from "cx/data"; import { bind, LabelsLeftLayout } from "cx/ui"; import { Slider } from "cx/widgets"; import { Square } from "./Square"; interface PageModel { red: number; green: number; blue: number; } const m = createModel(); export default (
); ``` ## React JSX Pragma Widget files must use the React JSX pragma because the `render` method returns React elements: ```tsx /** @jsxImportSource react */ ``` ## Config Interface Define a config interface for your component's properties. Use prop types like `StringProp`, `NumberProp`, `BooleanProp` for bindable properties: ```tsx interface SquareConfig extends WidgetConfig { red?: NumberProp; green?: NumberProp; blue?: NumberProp; } ``` ## Widget Class Extend `Widget` (or another base class) with your config type. Use `declare` for properties to avoid overwriting config values: ```tsx class Square extends Widget { declare red: number; declare green: number; declare blue: number; } ``` ## Widget Methods ### declareData Register bindable properties. Properties declared here can use data binding: ```tsx declareData(...args) { super.declareData(...args, { red: undefined, green: undefined, blue: undefined, }); } ``` Use `{ structured: true }` for object properties with bindable sub-properties. ### render Returns React elements for the component's visual representation: ```tsx render(context: RenderingContext, instance: Instance, key: string) { const { data } = instance; return (
); } ``` Parameters: - `context` - Passes information between parent and child widgets - `instance` - Contains `data`, `store`, and instance-specific properties - `key` - Unique identifier for React reconciliation ### init Called once when the widget class is initialized, before any rendering. ### initInstance Called for each widget instance. Use this to set up instance-specific data: ```tsx initInstance(context: RenderingContext, instance: Instance) { instance.customData = {}; super.initInstance(context, instance); } ``` ### initState Sets the initial internal state for stateful components. ### explore Evaluates data-bound attributes and explores children. Called before `prepare`: ```tsx explore(context: RenderingContext, instance: Instance) { super.explore(context, instance); // Access instance.data here } ``` ### prepare Additional preparation after `explore`, before `render`. Use to get information from context. ### cleanup Called after rendering to perform cleanup work. ## Prototype Defaults Set default property values on the prototype: ```tsx Square.prototype.red = 0; Square.prototype.green = 0; Square.prototype.blue = 0; ``` ## Base Classes | Base Class | Use Case | | --------------- | ---------------------------------------------------- | | `Widget` | Basic widgets | | `HtmlElement` | Widgets rendering HTML elements with styling support | | `Field` | Form input widgets with validation | | `PureContainer` | Containers without HTML wrapper | | `Container` | Containers with HTML wrapper | --- # Breaking Changes Sometimes we are forced to introduce breaking changes to the framework. This page will provide information about breaking changes and how to migrate your applications to the latest versions of the framework. ## 26.3.1 - Modern Sass Modules CxJS 26.3.1 migrates all SCSS files from the deprecated `@import` syntax to modern Sass modules (`@use` and `@forward`). This affects how projects import CxJS styles and theme packages. ### Why This Change? Sass has deprecated `@import` in favor of `@use` and `@forward`. The old `@import` system uses global scope, leading to naming conflicts and unpredictable load order. The new module system provides explicit dependencies, better encapsulation, and access to modern Sass features like `sass:color` and `sass:math`. ### Package Upgrades Required All CxJS theme packages have been restructured. Upgrade to the latest versions: | Package | Description | |---------|-------------| | `cx` | Core framework | | `cx-theme-aquamarine` | Aquamarine theme | | `cx-theme-dark` | Dark theme | | `cx-theme-frost` | Frost theme | | `cx-theme-material` | Material theme | | `cx-theme-material-dark` | Material Dark theme | | `cx-theme-packed-dark` | Packed Dark theme | | `cx-theme-space-blue` | Space Blue theme | | `cx-theme-variables` | **New** — CSS custom properties theme | ### Migration: Projects Without a Theme If your project imports CxJS styles directly without a theme: **Before:** ```scss @use 'sass:math'; $cx-include-global-rules: true; @import "~cx/src/variables"; @function cx-divide($a, $b) { @return math.div($a, $b); } @import "~cx/src/index"; ``` **After:** ```scss @use "cx/src/variables" as *; @use "cx/src/index" as *; ``` Key changes: - Replace `@import` with `@use ... as *` - Remove the `~` prefix (not needed with modern bundlers) - Remove the `cx-divide` compatibility shim (no longer needed) - `$cx-include-global-rules` is now set through the forward chain (see variable overrides below) ### Migration: Projects Using a Theme If your project uses one of the CxJS theme packages: **Before:** ```scss @use 'sass:math'; $cx-include-global-rules: true; @import "~cx-theme-frost/src/variables"; @function cx-divide($a, $b) { @return math.div($a, $b); } @import "~cx-theme-frost/src/index"; ``` **After:** ```scss @use "cx-theme-frost/src/index"; ``` That's it. Theme packages now handle all variable and map configuration internally. The `cx-divide` shim and `$cx-include-global-rules` are no longer needed — themes set these through the module system. ### Migration: Projects With Custom Variable Overrides If your project customizes theme variables, use the `@forward...with()` pattern. **Before:** ```scss $cx-default-color: #333; $cx-default-border-radius: 8px; @import "~cx-theme-frost/src/variables"; @import "~cx-theme-frost/src/index"; ``` **After (single file):** ```scss // variables.scss @forward "cx-theme-frost/src/variables" with ( $cx-default-color: #333 !default, $cx-default-border-radius: 8px !default ); ``` ```scss // index.scss @use "./variables" as *; @use "cx-theme-frost/src/maps" as *; @use "cx-theme-frost/src/overrides"; ``` The `@forward...with()` mechanism configures variables at load time. Values marked `!default` respect earlier overrides, enabling a clean three-layer configuration chain: **app → theme → framework**. ### Migration: Custom Themes If you maintain a custom CxJS theme, restructure it to follow the new module pattern: ``` my-theme/src/ ├── index.scss # Entry point ├── variables.scss # @forward cx/src/variables with (overrides) ├── variables.reset.scss # @forward ./variables with ($cx-include-global-rules: true) ├── maps.scss # @forward cx/src/maps + deep-merge overrides └── overrides.scss # CSS overrides (loads cx/src/index) ``` **index.scss** — Entry point that loads everything in the correct order: ```scss @use "sass:map"; // 1. Load theme variables (configures cx variables via @forward...with()) @use "./variables.reset" as *; // 2. Load theme maps (configures cx maps via deep-merge) @use "./maps" as *; // 3. Load theme overrides (loads cx/src/index + CSS overrides) @use "./overrides"; ``` **variables.reset.scss** — Enables global rules and forwards variables: ```scss @forward "./variables" with ($cx-include-global-rules: true); ``` **variables.scss** — Define overrides and forward to the framework: ```scss @use "sass:color"; // Theme-specific variables $my-primary: #3f51b5 !default; // Forward framework variables with theme defaults @forward "cx/src/variables" with ( $cx-default-color: #333 !default, $cx-default-border-radius: 4px !default, $cx-default-button-background-color: $my-primary !default ); ``` **maps.scss** — Override state style maps: ```scss @forward "cx/src/maps"; @use "./variables" as *; @use "cx/src/maps" as *; @use "cx/src/util/scss/deep-merge" as *; $cx-button-state-style-map: cx-deep-map-merge( $cx-button-state-style-map, ( hover: (background-color: color.adjust($my-primary, $lightness: -5%)) ) ); ``` **overrides.scss** — Load the framework and add CSS overrides: ```scss @use "./variables" as *; @use "cx/src/index" as *; .cxb-button { text-transform: uppercase; } ``` ### Deprecated Sass Functions Replace deprecated color functions with their modern equivalents: | Deprecated | Modern | |-----------|--------| | `darken($color, 10%)` | `color.adjust($color, $lightness: -10%)` | | `lighten($color, 10%)` | `color.adjust($color, $lightness: 10%)` | | `transparentize($color, 0.5)` | `color.adjust($color, $alpha: -0.5)` | | `$a / $b` (division) | `math.div($a, $b)` | Add `@use "sass:color";` and `@use "sass:math";` at the top of files that use these functions. CxJS also provides CSS variable-aware alternatives: `cx-lighten()`, `cx-darken()`, and `cx-calc()` from `cx/src/util/scss/calc`. These support both regular values and CSS custom properties. ### New Theme: cx-theme-variables This release introduces [`cx-theme-variables`](https://www.npmjs.com/package/cx-theme-variables), a new theme built entirely on CSS custom properties. Unlike traditional themes where colors are baked in at compile time, this theme outputs `var(--cx-...)` references, enabling runtime theme switching (e.g., light/dark mode) without recompilation. Try it out in the [Theme Editor](/themes). ## 26.2.0 ### LookupField: Improved TypeScript discrimination The `LookupField` component now uses TypeScript discriminated unions based on the `infinite` and `multiple` props. This provides better type inference and catches invalid prop combinations at compile time. #### `onQueryPage` replaces `onQuery` for infinite mode When using `LookupField` with `infinite: true`, you must now use the `onQueryPage` prop instead of `onQuery`. **Before:** ```tsx fetchData(query, page, pageSize)} /> ``` **After:** ```tsx fetchData(query, page, pageSize)} /> ``` The `onQuery` prop now only accepts a string query parameter for standard (non-infinite) mode: ```tsx fetchData(query)} /> ``` **Backwards Compatibility:** The runtime includes a compatibility shim that copies `onQuery` to `onQueryPage` when `infinite: true` is set. This allows existing code to continue working, but TypeScript will report type errors. We recommend updating your code to use the new API. #### `pageSize` is now exclusive to infinite mode The `pageSize` prop is now only available when `infinite: true`. If you were using `pageSize` without `infinite`, remove it as it had no effect. #### Props are now discriminated by `multiple` - When `multiple: true`: use `records` and/or `values` props - When `multiple` is not set or `false`: use `value` and `text` props TypeScript will now report errors if you mix props from different modes (e.g., using `value` with `multiple: true`). ## 26.1.0 ### TypeScript Migration The CxJS framework has been fully migrated to TypeScript. This is a major change that brings improved type safety, better IDE support, and enhanced developer experience. ### Separation from React JSX Types CxJS now provides its own JSX type definitions instead of relying on React's JSX types. This separation was necessary because CxJS JSX has fundamental differences from React JSX. To use the new JSX types, update your `tsconfig.json`: ```json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "cx" } } ``` With this configuration, TypeScript will use CxJS-specific JSX types, providing proper type checking for CxJS attributes and components. ### Package Upgrades Required The following packages have been updated and should be upgraded to version 26.x: | Package | Description | | ----------------------------------- | ------------------------------------------- | | `cx` | Core framework package | | `cx-react` | React adapter (now written in TypeScript) | | `babel-plugin-transform-cx-imports` | Babel plugin for optimizing imports | | `swc-plugin-transform-cx-jsx` | SWC plugin for CxJS JSX transformation | | `swc-plugin-transform-cx-imports` | SWC plugin for optimizing imports | | `cx-scss-manifest-webpack-plugin` | Webpack plugin for SCSS manifest generation | Some projects have copied `cx-scss-manifest-webpack-plugin` and used it in source form. These projects should now transition to using the official npm package instead. These versions will not work with the 26.x releases due to internal path changes and CSS will appear broken. Alternatively, the plugin can be patched to use the `build` folder instead of the `src` folder to detect which components are actually being used in the project. ### React 18+ Required CxJS 26.x requires React 18 or later. The framework now uses the modern React 18 APIs including `createRoot` from `react-dom/client`. If your application is still using React 17 or earlier, you will need to upgrade React before upgrading to CxJS 26.x. ### Migration Guide For a comprehensive guide on migrating your applications to TypeScript and taking advantage of the new type system, please refer to the [Migration Guide](/intro/migration-guide). --- ## 24.10.0 ### Legend and LegendEntry rendering Legend and LegendEntry components have been refactored to use the flexbox layout. This change might affect the appearance if you have custom styles applied to these components. ## 24.5.1 ### Default Window body padding The Window body now have a default padding. Previously, a few options were used to add padding such as `bodyStyle`, `bodyClass`, or adding margin/padding to the inner content. All these options lead to problems with layout consistency across different themes. If you want to revert to the old behavior, you can set the `pad` property to `false` on the prototype of the Window widget. This will effectively remove the default padding from the Window body, unless `pad` is explicitly set to `true` on the Window. ```javascript import { Window } from "cx/widgets"; Window.prototype.pad = false; ``` Alternatively, you can reset the Sass variable to remove default padding, before importing the CxJS variables. ```scss $cx-default-window-body-padding: 0; @import "~cx/src/variables"; ``` However, the best way forward would be to go through your codebase and remove `bodyStyle`, `bodyClass` values, or padding/margins used for this purpose within the window content. ## 23.2.0 ### Dropped support for Internet Explorer If you need to support Internet Explorer please use an older version of CxJS. ### Dart Sass Transition CxJS theming support is based on Sass. Since the beginning, the `node-sass` package was used to compile `.scss` files to CSS. This package is [deprecated](https://sass-lang.com/blog/libsass-is-deprecated) for some time and we're gradually replacing it with the [`sass`](https://www.npmjs.com/package/sass) package which doesn't rely on native code and therefore offers less compatibility problems, but has some of its own quirks. In the first phase both `node-sass` and `sass` will be supported. Later on, we're going to make a permanent switch after which `node-sass` will not work anymore. These are the steps required to start using `sass` today in your project: 1. remove `node-sass` from `package.json` 2. install `sass` 3. make the following changes in the root `index.scss` file: - add `@use 'sass:math';` at the top of the file - replace `cx-divide` function, after it's been imported ```scss @use 'sass:math'; # define variables ... @import '~cx/src/variables'; @function cx-divide($a, $b) { @return math.div($a, $b); } ``` Voila, your project now compiles CSS using `sass`. No more annoying `node-sass` issues. ## 21.3.0 ### Babel 7 The source code now uses the optional chaining operator. Please upgrade Babel to the latest version or add this plugin to your existing configuration. ### JSX runtime This release contains a new version of `babel-preset-cx-env` plugin which uses the new React JSX transform. This should result in slightly smaller bundle sizes and in some cases it's not required to import VDOM for React components. For more information check [this post on the official React blog](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html). The new release also removes Babel plugins which are now part of the @babel/preset-env preset out of the box, i.e. `@babel/transform-object-spread`. ### Whitespace trimming on generated `cx` code There are new version of `babel-plugin-cx-env` and `babel-plugin-transfrom-cx-jsx` which allow whitespace trimming in the generated code. This might help a bit with the generated bundle sizes. You can set this up in your `babel-config.js` file: ```javascript { presets: [ [ "babel-preset-cx-env", { cx: { jsx: { trimWhitespace: true, trimWhitespaceExceptions: ["Md", "CodeSnippet", "CodeSplit"], }, imports: { useSrc: true, }, }, }, ], ]; } ``` For more information, check the NPM page for [babel-plugin-transform-cx-jsx](https://www.npmjs.com/package/babel-plugin-transform-cx-jsx). ## 21.1.0 ### Change in invokeParentMethod Previously [`invokeParentMethod`](/concepts/controllers#-code-invokeparentmethod-code-) could be used to invoke Controller's own method. If the specified method was not found on current Controller instance, parent instances would be checked until the one with the specified method is found. With this change, `invokeParentMethod` now **skips** the current Controller instance and tries to invoke the specified method in one of the parent instances, as the name suggests. This can cause the code to break if, for example, `invokeParentMethod` was used in one of the inline event handlers: ```javascript
``` To fix this, make the following change in the `onClick` handler: ```javascript onClick={(e, instance) => { let controller = instance.controller; // Use invokeMethod instead of invokeParentMethod controller.invokeMethod('onSubmit', 1); }} ``` [`invokeMethod`](/concepts/controllers#-code-invokemethod-code-) has the same behaviour as the previous implementation of `invokeParentMethod`, hence it can be used as a fail-safe replacement for `invokeParentMethod` in this version of CxJS. ## 20.1.0 ### Format change for DateTimeField `DateTimeField` now expects regular formats, e.g. `datetime;yyyyMMMdd` (previously only `yyyyMMMdd` part was required). This change enables non-standard, custom formats to be used. ## 19.1.0 ### Babel 7 Starting with this version CxJS tooling requires Babel 7. New versions of the `babel-preset-cx-env`, `babel-plugin-transform-cx-jsx`, and `babel-plugin-transform-cx-imports` packages do not support Babel 6 anymore. These are the steps required to migrate your applications to Babel 7: In `package.json`, update the following packages: - `"babel-core"` => `"@babel/core": "^7.2.2"`, - `"babel-preset-env"` => `"@babel/preset-env": "^7.2.3"` - `"babel-polyfill"` => `"@babel/polyfill": "^7.2.5"` In `babel.config`, replace `useBuiltIns: true` with `useBuiltIns: 'usage'`. In `polyfill.js`, remove `import "babel-polyfill";` If some other Babel plugins are used please make sure that these are also upgraded to versions which target Babel 7. That's it. #### TypeScript One of the benefits that Babel 7 brings is support for TypeScript without the TypeScript tooling. You can easily enable TypeScript in your project by installing the `@babel/preset-typescript` npm package and registering the preset in your `babel.config` file. You'll also have to tweak rules in `webpack.config.js` to support `.ts` and `.tsx` files. Replace ```javascript test: /\.js$/, loader: 'babel-loader', ``` with: ```javascript test: /\.(js|ts|tsx)$/, loader: 'babel-loader', ``` You can now mix `.js`, `.ts` and `.tsx` files. However, some of the [JSX in TS related quirks still apply](https://github.com/codaxy/cx-typescript-boilerplate). ## 18.12.0 ### Functional Components and CxJS attributes In order to support [store refs](https://github.com/codaxy/cxjs/issues/487) some changes were made to how functional components handle CxJS-specific attributes such as `visible`, `controller` and `layout`. For example, let's take a simple Tab component. ```javascript const TabCmp = ({ prop1, children }) => (
{children}
); ``` In previous versions of CxJS, if the `visible` attribute is used on a functional component, it would be applied on all top-level elements. ```javascript Tab1 Content ``` This example above would expand to: ```javascript
Tab1 Content
``` From this version, a PureContainer wrapper is added to all functional components and all CxJS-specific attributes are applied on the wrapper element. ```javascript
Tab1 Content
``` Please note that this is a breaking change only if top-level component is `Rescope`, `Restate` or `DataProxy`. With this change, both functional components and functional controllers can receive the `store` prop which enables [an alternative syntax for accessing data using store references](https://github.com/codaxy/cxjs/issues/487). ## 17.12.0 ### `babel-preset-env` `babel-preset-env` is now a peer dependency of `babel-preset-cx-env`. Therefore it needs to be installed in your project. This change enables the `babel-preset-env` package to be updated independently from the `babel-preset-cx-env` package. ``` npm install babel-preset-env --saveDev yarn add babel-preset-env --dev ``` ### `-bind`, `-tpl`, `-expr` syntax Data-binding attributes can now be written in an alternative syntax with a dash instead of a colon, for example `value:bind` instead of `value-bind`. Although not necessarily a breaking change, both methods are supported which solves a long standing problem of syntax errors that [Visual Studio Code](https://code.visualstudio.com) reports if XML namespaces are used inside JSX. ## 17.7.0 This release adds support for CxJS applications with an extremely short start up time such as [CxJS Hacker News](https://github.com/codaxy/cxjs-hackernews). Bigger applications will improve startup time through incremental app loading and adopting [the app shell architecture](https://developers.google.com/web/fundamentals/architecture/app-shell). In order for us to support minimal application shells some internal CxJS dependencies had to be broken. ### Confirmation Dialogs The `Button` requires `MsgBox` and `Window` components in order to support user confirmation dialogs. This (`confirm`) function is not always necessary, but when needed. it's better to load these additional classes after the application launch. In order to enable CxJS based confirmation dialogs, use the `enableMsgBoxAlerts` method. Otherwise, the browser default `prompt` dialog will appear. To enable the confirmation function on application startup, use the following snippet: ```javascript import { enableMsgBoxAlerts } from "cx/widgets"; enableMsgBoxAlerts(); ``` ### Tooltips Tooltips are not automatically loaded anymore. The following example will not work because tooltips first need to be enabled using the `enableTooltips` method. ```jsx
``` Use the following code to enable tooltips: ```javascript import { enableTooltips } from "cx/widgets"; enableTooltips(); ``` ### Culture-Sensitive Number, Date and Currency Formatting Culture-sensitive formats for dates and numbers are not automatically registered. Formatting is auto-enabled if `NumberField`, `DateField` or any other culture dependent widget is used; otherwise it needs to be enabled using the `enableCultureSensitiveFormatting` method. ```javascript import { enableCultureSensitiveFormatting } from "cx/ui"; enableCultureSensitiveFormatting(); ``` ### Fat Arrow Expansion In order to support fat arrows in expressions CxJS includes a transformer which rewrites fat arrows into the standard function notation. This allows fat arrows to be used in Internet Explorer and older versions of Safari, like in the following example. ```jsx
``` Code from the snippet above will not work in IE anymore because fat arrow expansion is now optional and needs to be enabled using the `enableFatArrowExpansion` method. ```javascript import { enableFatArrowExpansion } from "cx/data"; enableFatArrowExpansion(); ``` ### Enable All For apps that do not use code-splitting and the developers want to enable all internal dependencies, you may use `enableAllInternalDependencies` and everything will be as it was in previous versions. ```javascript import { enableAllInternalDependencies } from "cx/widgets"; enableAllInternalDependencies(); ``` ## 17.4.0 We're proud to announce that we obtained ownership of the `cx` package at [npmjs](https://www.npmjs.com/package/cx) and therefore our `cx-core` package will be replaced with `cx` and deprecated. To migrate your apps, please do the following: In `package.json` replace `cx-core` with `cx`. ``` yarn remove cx-core yarn add cx ``` Additionally, if `babel-plugin-transform-cx-imports` is used with `useSrc` option, in `webpack.config.js` `cx` package should be whitelisted instead of `cx-core` in the `babel-loader` configuration. ```js test: /\.js$/, loader: 'babel-loader', include: /(app|cx)/, //previously (app|cx-core) ``` If `cx-core` reference is used in `.scss` files, replace it with `cx`. ```scss @import "~cx/src/variables"; //cx-core => cx @import "~cx/src/index"; //cx-core => cx ``` After you're done, please upgrade all Cx related packages to the latest version. ```bash yarn upgrade-interactive ``` Also, upgrade `cx-cli` tools globally. ```bash yarn global add cx-cli ``` That's it. The `cx-core` package will continue to work, but we recommend that all users to switch to the new package. The benefit of this change is that the code completion will now work as IDEs will now be able to find the `cx` package. --- # Migration Guide Starting with CxJS 26.x, the core framework has been migrated to TypeScript. This guide covers the patterns and best practices for working with TypeScript in CxJS applications, whether you're migrating an existing project or starting fresh. ## Project Setup ### TypeScript Configuration Configure your `tsconfig.json` with the following settings for optimal CxJS support: ```json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "cx", "moduleResolution": "bundler", "esModuleInterop": true } } ``` The key setting is `"jsxImportSource": "cx"`. CxJS now provides its own JSX type definitions instead of relying on React's JSX types. This means CxJS-specific attributes like `visible`, `controller`, `layout`, and data-binding functions (`bind()`, `expr()`, `tpl()`) are properly typed without conflicts with React's typings. ### Webpack Configuration With TypeScript, you no longer need `babel-loader` or special Babel plugins for CxJS. Simply use `ts-loader` to handle TypeScript files: ```javascript { test: /\.(ts|tsx)$/, loader: 'ts-loader', exclude: /node_modules/ } ``` ### Vite Support CxJS also supports Vite as a build tool. Vite provides faster development experience with hot module replacement. Configure Vite with the appropriate React plugin and ensure `jsxImportSource` is set to `cx` in your configuration. ### Without `transform-cx-jsx` Plugin Applications should continue to work with the `transform-cx-jsx` plugin enabled. However, if you want to run your application without this plugin, the following requirements apply: 1. All functional components must be wrapped in `createFunctionalComponent` calls 2. The special JSX prop syntax (`-bind`, `-expr`, `-tpl`) must be converted to function calls (`bind()`, `tpl()`, `expr()`) or object form like `{{ bind: "prop" }}`, `{{ tpl: "template" }}`, or `{{ expr: "1+1" }}` 3. All components previously developed in JavaScript must be ported to TypeScript. ### Bundle Size Optimization (Optional) While not required, you can use `babel-plugin-transform-cx-imports` to minimize bundle size by transforming CxJS imports to more specific paths: ```bash # Install the plugin npm install babel-plugin-transform-cx-imports --save-dev # In babel.config.js { plugins: [ ["transform-cx-imports", { useSrc: true }] ] } ``` If using this plugin, chain `babel-loader` after `ts-loader`: ```javascript { test: /\.(ts|tsx)$/, exclude: /node_modules/, use: ['babel-loader', 'ts-loader'] } ``` ## General Improvements ### Renamed: createModel The `createAccessorModelProxy` function has been renamed to `createModel` for brevity. The old name remains available as an alias for backward compatibility, but `createModel` is now preferred. ```typescript // Before import { createAccessorModelProxy } from "cx/data"; const m = createAccessorModelProxy(); // After (preferred) import { createModel } from "cx/data"; const m = createModel(); ``` ### Typed Controller Methods With TypeScript, you can use `getControllerByType` to get a typed controller reference instead of using string method names. This provides compile-time safety and IDE autocomplete. ```typescript import { Controller, bind } from "cx/ui"; import { Button, Section } from "cx/widgets"; class PageController extends Controller { onSave() { // save logic } onDelete(id: string) { // delete logic } } export default (
{/* Type-safe controller method calls */}
); ``` The `getControllerByType(ControllerClass)` method searches up the widget tree and returns a typed controller instance, enabling full autocomplete and compile-time type checking for controller methods and their parameters. ### Typed RenderingContext CxJS uses a `RenderingContext` object to pass information down the widget tree during rendering. Different widget families define typed context interfaces that extend `RenderingContext` for type-safe access to context properties. **Available typed contexts:** - `FormRenderingContext` - Form validation context (`parentDisabled`, `parentReadOnly`, `validation`, etc.) - `SvgRenderingContext` - SVG layout context (`parentRect`, `inSvg`, `addClipRect`) - `ChartRenderingContext` - Chart context extending SVG (`axes`) When creating custom widgets that consume these context properties, import and use the typed context interface in your method signatures: ```typescript import type { FormRenderingContext } from "cx/widgets"; export class MyFormWidget extends Field { explore(context: FormRenderingContext, instance: Instance) { // Type-safe access to form context properties if (context.parentDisabled) { // handle disabled state } super.explore(context, instance); } } ``` ### Typed ContentResolver The `ContentResolver` widget now supports type inference for the `onResolve` callback params. TypeScript automatically infers the resolved types from your params definition: ```typescript import { ContentResolver } from "cx/widgets"; import { createModel } from "cx/data"; interface AppModel { user: { name: string; age: number }; } const model = createModel(); age: model.user.age, // AccessorChain limit: 10, // number literal }} onResolve={(params) => { // TypeScript infers: // params.name: string // params.age: number // params.limit: number return
{params.name} is {params.age} years old
; }} /> ``` **Type resolution behavior:** | Param Type | Resolved Type | | ------------------------------- | ----------------------------------- | | Literal values (`42`, `"text"`) | Preserves type (`number`, `string`) | | `AccessorChain` | `T` | | `Selector` / `GetSet` | `T` | | `bind()` / `tpl()` / `expr()` | `any` (runtime-only) | The utility types `ResolveProp

` and `ResolveStructuredProp` are exported from `cx/ui` if you need to use them in your own generic components. ### Expression Helpers CxJS provides type-safe selector functions for reactive bindings. These helpers return `Selector` which can be used anywhere a boolean binding is expected: ```typescript import { truthy, isEmpty, equal, greaterThan } from "cx/ui"; import { createModel } from "cx/data"; interface AppModel { user: { name: string; age: number }; items: string[]; } const model = createModel(); // Using expression helpers for type-safe boolean bindings

User has a name
No items available
User is an adult
``` **Available expression helpers:** | 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 Helper The `format` helper creates a selector that formats values using CxJS format strings. This is useful for displaying formatted numbers, dates, or percentages in text props: ```typescript import { createModel } from "cx/data"; import { format } from "cx/ui"; interface Product { name: string; price: number; discount: number; } const m = createModel(); // Format as number with 2 decimal places
// Format as percentage
// With custom null text
``` The format string uses CxJS format syntax (e.g., `"n;2"` for numbers, `"p;0"` for percentages, `"d"` for dates). The optional third parameter specifies text to display for null/undefined values. ### Template Helper with Accessor Chains The `tpl` function now supports accessor chains in addition to its original string-only form. This allows you to create formatted strings from multiple values with full type safety: ```typescript import { createModel } from "cx/data"; import { tpl } from "cx/ui"; interface Person { firstName: string; lastName: string; age: number; } const m = createModel(); // Original string-only form still works
// New accessor chain form with positional placeholders
// Supports formatting in placeholders
// Supports null text
``` The accessor chain form uses positional placeholders (`{0}`, `{1}`, etc.) and supports all StringTemplate features including formatting (`:format`) and null text (`|nullText`). ### Typed Config Properties Several widget config properties now have improved type definitions that provide better autocomplete and type checking when using the `type` or `$type` pattern. #### Selection Grid, PieChart, and BubbleGraph support typed `selection` configs: ```typescript import { Grid } from "cx/widgets"; import { KeySelection } from "cx/ui"; ``` Supported selection types: `Selection`, `KeySelection`, `PropertySelection`, `SimpleSelection`. #### Chart Axes Chart axes support typed configs for different axis types: ```typescript import { Chart } from "cx/charts"; import { NumericAxis, CategoryAxis } from "cx/charts"; {/* chart content */} ``` Supported axis types: `Axis`, `NumericAxis`, `CategoryAxis`, `TimeAxis`. #### Data Adapters Grid and List support typed `dataAdapter` configs: ```typescript import { Grid } from "cx/widgets"; import { GroupAdapter } from "cx/ui"; ``` Supported adapter types: `ArrayAdapter`, `GroupAdapter`, `TreeAdapter`. #### Dropdown Options Form fields with dropdowns (ColorField, DateTimeField, MonthField, LookupField) accept typed `dropdownOptions`: ```typescript import { DateTimeField } from "cx/widgets"; ``` #### Typed Controllers The `controller` property accepts multiple forms: a class, a config object with `type`/`$type`, an inline config, or a factory function. Because this type is intentionally flexible ("open"), TypeScript's generic inference may not catch extra or misspelled properties in config objects. Use the `validateConfig` helper to enable strict property checking: ```typescript import { validateConfig } from "cx/util"; import { Controller } from "cx/ui"; interface MyControllerConfig { apiEndpoint: string; maxRetries: number; } class MyController extends Controller { declare apiEndpoint: string; declare maxRetries: number; constructor(config?: MyControllerConfig) { super(config); } } // validateConfig enables strict checking
``` The `validateConfig` function is a compile-time helper that returns its input unchanged at runtime. It can be used with any config object that follows the `{ type: Class, ...props }` pattern. ## Authoring Widgets Previously, CxJS widgets had to be written in JavaScript with optional TypeScript declaration files (`.d.ts`) for typing. With CxJS 26.x, you can now author widgets entirely in TypeScript. > **Important:** Widget files must use the `/** @jsxImportSource react */` pragma because > the widget's `render` method uses React JSX. ### Complete Widget Example Here's a complete example showing all the steps to create a CxJS widget in TypeScript: ```typescript /** @jsxImportSource react */ import { BooleanProp, StringProp, RenderingContext, VDOM } from "cx/ui"; import { HtmlElement, HtmlElementConfig } from "cx/widgets"; // 1. Define the Config interface export interface MyButtonConfig extends HtmlElementConfig { icon?: StringProp; pressed?: BooleanProp; } // 2. Extend the appropriate generic base class (Instance type argument is optional) export class MyButton extends HtmlElement { // 3. Use declare for all properties from config/prototype declare icon?: string; declare pressed?: boolean; declare baseClass: string; // 4. Declare bindable props in declareData declareData(...args) { super.declareData(...args, { icon: undefined, pressed: undefined, }); } // 5. Add constructor accepting the config type constructor(config?: MyButtonConfig) { super(config); } // 6. Implement render method with React JSX render( context: RenderingContext, instance: Instance, key: string ): React.ReactNode { return ( ); } } // 7. Initialize prototype properties MyButton.prototype.baseClass = "mybutton"; ``` ### Key Steps 1. **Add React JSX pragma** - Use `/** @jsxImportSource react */` at the top of widget files 2. **Define Config interface** - Name it `[WidgetName]Config` and extend the parent's config 3. **Extend generic base class** - Use `HtmlElement`, `ContainerBase`, etc. 4. **Use `declare` for properties** - Prevents TypeScript from overwriting config/prototype values 5. **Declare bindable props in `declareData`** - Register props that support data binding 6. **Add typed constructor** - Accepts the config type for proper type inference 7. **Implement render method** - Returns React JSX elements ### Config Property Types Use these types for bindable properties in your Config interface: | Type | Usage | | ------------- | ---------------------------------- | | `StringProp` | Bindable string property | | `BooleanProp` | Bindable boolean property | | `NumberProp` | Bindable number property | | `Prop` | Bindable property of custom type T | | `RecordsProp` | Array data (Grid, List) | ### Using `declare` for Properties > **Important:** Widget properties must use `declare` to avoid being overwritten. Without `declare`, > TypeScript class fields will override values passed through the config (via `Object.assign` in the > constructor) or values defined on the prototype. ```typescript // WRONG - these fields will override config values with undefined export class MyWidget extends HtmlElement { icon?: string; // Overwrites config.icon! pressed?: boolean; // Overwrites config.pressed! } // CORRECT - declare tells TypeScript the field exists without initializing it export class MyWidget extends HtmlElement { declare icon?: string; declare pressed?: boolean; declare baseClass: string; // Non-nullable when defined in prototype } ``` ### Base Classes CxJS provides generic base classes for creating typed widgets. The second type argument (Instance) is optional: | Base Class | Use Case | | --------------------------- | -------------------------------- | | `HtmlElement` | Widgets rendering HTML elements | | `ContainerBase` | Widgets containing other widgets | | `PureContainerBase` | Containers without HTML wrapper | | `Field` | Form input widgets | ### Custom Instance Types When a widget needs custom properties on its instance, create a custom instance interface: ```typescript export interface MyWidgetInstance extends Instance { customData: SomeType; } export class MyWidget extends HtmlElement { initInstance(context: RenderingContext, instance: MyWidgetInstance): void { instance.customData = initializeSomething(); super.initInstance(context, instance); } } ``` ### Migration Checklist When migrating a widget from JavaScript to TypeScript: 1. Add JSX pragma `/** @jsxImportSource react */` if file contains JSX 2. Create `[WidgetName]Config` interface extending appropriate parent 3. Add generic type parameters to base class if needed 4. Add constructor accepting the config type 5. Add `declare` statements for all class properties 6. Add type annotations to all methods 7. Create custom instance interface if needed 8. Fix prototype initializations (use `undefined` not `null` where needed) 9. Declare `baseClass` as non-nullable if defined in prototype 10. Delete the corresponding `.d.ts` file ### File Organization After migration, each widget should have: - `Widget.tsx` - The implementation with inline types - No separate `Widget.d.ts` - Types are in the source file Index files (`index.ts`) should re-export all public types: ```typescript export { Button, ButtonConfig } from "./Button"; export { FlexBox, FlexBoxConfig } from "./FlexBox"; ``` --- # Sample Applications Explore full-featured applications built with CxJS to see how various framework features work together in real-world scenarios. | Application | Description | Demo | Source | | -------------------- | ------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------- | | Tdo | Task management app with forms, lists, and local storage | [App](https://tdoapp.com) | [GitHub](https://github.com/codaxy/tdo) | | Hacker News | HN clone with routing, data fetching, and infinite scrolling | [App](https://hn.cxjs.io) | [GitHub](https://github.com/codaxy/cxjs-hackernews) | | State of JS Explorer | Interactive visualization of JavaScript survey data | [App](https://codaxy.github.io/state-of-js-2016-explorer) | [GitHub](https://github.com/codaxy/state-of-js-2016-explorer) | | Starter Kit | Admin/dashboard boilerplate with routing and charts | [App](https://cxjs.io/starter) | [GitHub](https://github.com/codaxy/cx-starter-kit) | | Tailwind Template | CxJS + Tailwind CSS application template | [App](https://twapp.cxjs.io) | [GitHub](https://github.com/codaxy/cxjs-tailwindcss-template) | | Wealth Management | Enterprise dashboard with forms, tables, and charts | [App](https://wealth-management.cxjs.io) | [GitHub](https://github.com/codaxy/wealth-management-demo) | | Worldoscope | World Bank data reporting tool with customizable templates | [App](https://worldoscope.cxjs.io) | [GitHub](https://github.com/codaxy/worldoscope) | | Home Expenses | Expense tracking tutorial app with forms and charts | [App](https://home-expenses.cxjs.io) | [GitHub](https://github.com/codaxy/cxjs-home-expenses-app-tutorial) | --- # Component Libraries These are add-on libraries that extend CxJS with additional components for specific use cases. | Library | Description | Website | Source | | -------------------- | ------------------------------------------------------------ | ----------------------------------------- | -------------------------------------------------- | | CxJS Diagrams | SVG-based diagram library for flowcharts and shapes | [Website](https://diagrams.cxjs.io) | [GitHub](https://github.com/codaxy/cx-diagrams) | | Cx Google Maps | Google Maps wrapper with markers, polygons, and heatmaps | [Website](https://maps.cxjs.io) | [GitHub](https://github.com/codaxy/cx-google-maps) | --- # 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

Home

Welcome to the home page.

About

Learn more about us.

Contact

Get in touch with us.

Current URL:
); ``` ## Route Patterns Routes support parameters, splats, and optional parts using [route-parser](https://github.com/rcs/route-parser#what-can-i-use-in-my-routes) syntax: | Pattern | Description | Example Match | | --------------- | -------------------- | -------------------------------- | | `~/users` | Exact match | `/users` | | `~/users/:id` | Named parameter | `/users/123` → `id: "123"` | | `~/files/*path` | Splat (rest of path) | `/files/a/b/c` → `path: "a/b/c"` | | `~/users(/:id)` | Optional parameter | `/users` or `/users/123` | ## Prefix Matching Use `prefix` to match routes that start with a pattern: ```tsx {/* Matches ~/admin, ~/admin/users, ~/admin/settings, etc. */} ``` ## Nested Routes Use `+/` to define routes relative to the parent: ```tsx ``` ## Configuration | Property | Type | Description | | ---------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `route` / `path` | `string` | Target route, e.g. `~/user/:userId`. Use `~/` to denote the application root path and `+/` in nested routes to append to the parent route. | | `url` | `Prop` | Url binding. Bind this to the global `url` variable. | | `prefix` | `boolean` | Match route even if given `route` is only a prefix of the current `url`. Used when a route contains nested subroutes. | | `params` | `Prop` | Params binding. Matched route parameters will be stored inside. | --- # RedirectRoute ```ts import { RedirectRoute } from 'cx/widgets'; ``` RedirectRoute automatically redirects to another URL when matched. It uses `replaceState`, so the browser back button won't return to the redirected URL. ## Usage ```tsx ``` Common use cases: - Redirect from root to a default page - Redirect legacy URLs to new locations - Redirect unauthenticated users to login ## Configuration | Property | Type | Description | | ---------- | -------------- | -------------------------- | | `route` | `string` | Route pattern to match | | `url` | `Prop` | Binding to the current URL | | `redirect` | `string` | Target URL to redirect to | --- # Url ```ts import { Url } from 'cx/ui'; ``` The Url helper class provides methods for working with URL paths, particularly for resolving the `~/` prefix used in routes. ## Setting the Base Path Before using routes, set the application's base path. This is required if your app is hosted in a subdirectory: ```tsx // Set explicitly Url.setBase("/my-app/"); // Or detect from a script tag Url.setBaseFromScript("~/app.js"); ``` If not set, the default base path is `/`. ## Methods | Method | Description | | ----------------------------------- | ----------------------------------------------------------- | | `Url.setBase(base)` | Sets the base path of the application | | `Url.setBaseFromScript(scriptPath)` | Sets base path by finding a matching script `src` attribute | | `Url.resolve(path)` | Resolves `~/` to the application base path | | `Url.unresolve(path)` | Converts an absolute path back to `~/` format | | `Url.isLocal(path)` | Checks if a path is within the application | ## Examples ```tsx Url.setBase("/docs/"); Url.resolve("~/page"); // "/docs/page" Url.unresolve("/docs/page"); // "~/page" Url.isLocal("/docs/"); // true Url.isLocal("/other/"); // false ``` --- # History ```ts import { History } from 'cx/ui'; ``` The History class handles HTML5 `pushState` navigation, connecting the browser URL to your store. ## Setup Connect History to your store at application startup: ```tsx History.connect(store, "url"); ``` The store is updated whenever navigation happens, allowing the application to re-render and display relevant content. ## Methods | Method | Description | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `History.connect(store, bind)` | Initializes a link between browser's location and store variable pointed by the `bind` argument | | `History.pushState(state, title, url)` | Performs navigation to a new location without refreshing the page | | `History.replaceState(state, title, url)` | Performs navigation to a new location. Current location will not be saved in browser's history. | | `History.subscribe(callback)` | Subscribe to location changes. Useful for setting up page tracking (e.g. Google Analytics). Returns an unsubscribe function. | | `History.reloadOnNextChange()` | Instructs the router to reload the page on next navigation. Commonly used with service workers. | | `History.addNavigateConfirmation(callback, permanent)` | Instructs the router to execute the given callback to confirm leaving the current page. The callback is executed only for the current page, unless `permanent` is set to `true`. | ## Programmatic Navigation ```tsx // Add to history (back button will return here) History.pushState({}, null, "~/dashboard"); // Replace current entry (back button skips this) History.replaceState({}, null, "~/dashboard"); ``` ## Subscribing to Navigation Track page views or perform actions on navigation: ```tsx History.subscribe((url) => { analytics.trackPageView(url); }); ``` ## Navigation Confirmation Prompt users before they leave a page with unsaved changes: ```tsx History.addNavigateConfirmation((url) => { return MsgBox.yesNo("You have unsaved changes. Leave anyway?").then( (answer) => answer === "yes", ); }); ``` The callback receives the target URL and should return a boolean or `Promise`. By default, the confirmation is removed when leaving the page. Pass `true` as the second argument to make it permanent. ## Browser Support Browsers without `pushState` support fall back to standard navigation (full page reload). --- # Drag and Drop ```ts import { DragSource, DropZone, DragHandle } from 'cx/widgets'; ``` CxJS provides components for implementing drag and drop functionality: - [DragSource](/docs/concepts/drag-source) - Wraps elements that can be dragged - [DropZone](/docs/concepts/drop-zone) - Defines areas where items can be dropped - [DragHandle](/docs/concepts/drag-handle) - Optional handle to initiate dragging ## Example This example demonstrates list reordering using drag and drop: ```tsx import { createModel } from "cx/data"; import { Controller } from "cx/ui"; import { DragSource, DropZone, Repeater } from "cx/widgets"; interface Item { id: number; text: string; } interface PageModel { items: Item[]; $record: Item; $index: number; } const m = createModel(); class PageController extends Controller { onInit() { this.store.set(m.items, [ { id: 1, text: "Apple" }, { id: 2, text: "Banana" }, { id: 3, text: "Cherry" }, { id: 4, text: "Date" }, { id: 5, text: "Elderberry" }, ]); } } export default (
{ let targetIndex = store.get(m.$index); let items = store .get(m.items) .filter((item) => item.id !== source.data.id); store.set(m.items, [ ...items.slice(0, targetIndex), source.data, ...items.slice(targetIndex), ]); }} matchWidth matchHeight matchMargin inflate={50} /> { let items = store .get(m.items) .filter((item) => item.id !== source.data.id); store.set(m.items, [...items, source.data]); }} inflate={50} >
); ``` ## How It Works 1. Each item is wrapped in a `DragSource` that passes the record data when dragged 2. A `DropZone` around each item receives the dropped data 3. The `onDrop` handler reorders the array by removing the item from its old position and inserting it at the new position --- # DragSource ```ts import { DragSource } from 'cx/widgets'; ``` `DragSource` wraps elements that can be dragged. The `data` property specifies what data is passed to the drop zone when the element is dropped. ## Example ```tsx import { DragSource, DropZone } from "cx/widgets"; export default (
Drag me!
Or me!
{ alert(`Dropped: ${source.data.text}`); }} >
Drop here
); ``` ## Dashboard Example This example demonstrates advanced features: - `handled` - Only the handle initiates drag, allowing text selection - `clone` - Custom lightweight widget shown during drag instead of cloning the entire content ```tsx import { createModel } from "cx/data"; import { Controller } from "cx/ui"; import { DragHandle, DragSource, DropZone, Repeater } from "cx/widgets"; interface Widget { id: number; title: string; color: string; } interface PageModel { widgets: Widget[]; $widget: Widget; $index: number; } const m = createModel(); class PageController extends Controller { onInit() { this.store.set(m.widgets, [ { id: 1, title: "Sales", color: "bg-blue-100" }, { id: 2, title: "Orders", color: "bg-green-100" }, { id: 3, title: "Users", color: "bg-purple-100" }, ]); } } export default (
{ let targetIndex = store.get(m.$index); let widgets = store .get(m.widgets) .filter((w) => w.id !== source.data.id); store.set(m.widgets, [ ...widgets.slice(0, targetIndex), source.data, ...widgets.slice(targetIndex), ]); }} matchWidth matchHeight matchMargin inflate={100} /> } >
⋮⋮
1,234
Sample metric
{ let widgets = store .get(m.widgets) .filter((w) => w.id !== source.data.id); store.set(m.widgets, [...widgets, source.data]); }} inflate={100} >
); ``` ## Configuration | Property | Type | Description | | -------------- | ---------- | ---------------------------------------------------------------------------------- | | `data` | `Prop` | Data passed to the drop zone on drop. | | `hideOnDrag` | `boolean` | Hide the source element while dragging. Defaults to `false`. | | `handled` | `boolean` | If `true`, dragging is initiated only through a `DragHandle`. Defaults to `false`. | | `onDragStart` | `function` | Callback invoked when drag starts. Return `false` to cancel. | | `onDragEnd` | `function` | Callback invoked when drag ends. | | `clone` | `Config` | Custom widget to display during drag instead of cloning the source. | | `cloneStyle` | `Style` | CSS styles applied to the drag clone. | | `cloneClass` | `Class` | CSS class applied to the drag clone. | | `draggedStyle` | `Style` | CSS styles applied to the source element while being dragged. | | `draggedClass` | `Class` | CSS class applied to the source element while being dragged. | | `id` | `string` | ID attribute for the element. | --- # DropZone ```ts import { DropZone } from 'cx/widgets'; ``` `DropZone` defines areas where dragged items can be dropped. The `onDrop` callback receives the dropped data and allows you to handle the drop action. ## Example This example demonstrates `onDropTest` to restrict which items can be dropped in each zone. Fruits and Vegetables zones only accept matching types, while Anything accepts all items. ```tsx import { createModel } from "cx/data"; import { Controller } from "cx/ui"; import { DragSource, DropZone, Repeater } from "cx/widgets"; interface Item { name: string; type: "fruit" | "vegetable"; } interface PageModel { fruits: Item[]; vegetables: Item[]; anything: Item[]; $fruit: Item; $vegetable: Item; $item: Item; } const m = createModel(); function removeItem(store: any, item: Item) { store.set( m.fruits, store.get(m.fruits).filter((x: Item) => x.name !== item.name), ); store.set( m.vegetables, store.get(m.vegetables).filter((x: Item) => x.name !== item.name), ); store.set( m.anything, store.get(m.anything).filter((x: Item) => x.name !== item.name), ); } class PageController extends Controller { onInit() { this.store.set(m.fruits, [ { name: "Apple", type: "fruit" }, { name: "Banana", type: "fruit" }, ]); this.store.set(m.vegetables, [ { name: "Carrot", type: "vegetable" }, { name: "Broccoli", type: "vegetable" }, ]); this.store.set(m.anything, []); } } export default (
source.data.type === "fruit"} onDrop={({ source }, { store }) => { removeItem(store, source.data); store.set(m.fruits, [...store.get(m.fruits), source.data]); }} >
Fruits
source.data.type === "vegetable"} onDrop={({ source }, { store }) => { removeItem(store, source.data); store.set(m.vegetables, [...store.get(m.vegetables), source.data]); }} >
Vegetables
{ removeItem(store, source.data); store.set(m.anything, [...store.get(m.anything), source.data]); }} >
Anything
); ``` ## Sizing Options Control how the drop zone matches its content: ```tsx
Content
``` ## Block Mode Use `mod="block"` for invisible drop zones used in list reordering. In block mode, the drop zone has a height of 1px and spans the full width. Use `inflate` to expand the hit area: ```tsx ``` ## Configuration | Property | Type | Description | | -------------- | ---------- | ------------------------------------------------------------------ | | `data` | `Prop` | Bindable data related to the DropZone, available in callbacks. | | `onDrop` | `function` | Callback invoked when an item is dropped. | | `onDropTest` | `function` | Test if dragged source is acceptable. Return `false` to reject. | | `onDragStart` | `function` | Callback invoked when any drag operation begins. | | `onDragEnd` | `function` | Callback invoked when any drag operation ends. | | `onDragEnter` | `function` | Callback invoked when source enters the drop zone. | | `onDragLeave` | `function` | Callback invoked when source leaves the drop zone. | | `onDragOver` | `function` | Callback invoked continuously while source is over the zone. | | `onDragNear` | `function` | Callback invoked when source gets close to the zone. | | `onDragAway` | `function` | Callback invoked when source moves away from the zone. | | `overStyle` | `Style` | CSS styles applied when cursor is over the drop zone. | | `overClass` | `Class` | CSS class applied when cursor is over the drop zone. | | `nearStyle` | `Style` | CSS styles applied when cursor is near the drop zone. | | `nearClass` | `Class` | CSS class applied when cursor is near the drop zone. | | `farStyle` | `Style` | CSS styles applied when drag begins to highlight drop zones. | | `farClass` | `Class` | CSS class applied when drag begins to highlight drop zones. | | `nearDistance` | `number` | Distance in pixels to consider the cursor "near". Defaults to `0`. | | `inflate` | `number` | Inflate bounding box by this many pixels in all directions. | | `hinflate` | `number` | Inflate bounding box horizontally. | | `vinflate` | `number` | Inflate bounding box vertically. | | `matchWidth` | `boolean` | Match width of the item being dragged. | | `matchHeight` | `boolean` | Match height of the item being dragged. | | `matchMargin` | `boolean` | Match margin of the item being dragged. | --- # DragHandle ```ts import { DragHandle } from 'cx/widgets'; ``` `DragHandle` provides a specific area within a `DragSource` that initiates dragging. This allows the rest of the content to remain interactive (selectable text, clickable buttons, etc.) while still supporting drag and drop. ## Example Try selecting the text - only the handle on the left initiates drag: ```tsx import { createModel } from "cx/data"; import { Controller } from "cx/ui"; import { DragHandle, DragSource, DropZone, Repeater } from "cx/widgets"; interface Item { id: number; text: string; } interface PageModel { items: Item[]; $record: Item; $index: number; } const m = createModel(); class PageController extends Controller { onInit() { this.store.set(m.items, [ { id: 1, text: "Select this text without triggering drag" }, { id: 2, text: "Use the handle on the left to drag" }, { id: 3, text: "Content remains interactive" }, ]); } } export default (
{ let targetIndex = store.get(m.$index); let items = store .get(m.items) .filter((item) => item.id !== source.data.id); store.set(m.items, [ ...items.slice(0, targetIndex), source.data, ...items.slice(targetIndex), ]); }} matchWidth matchHeight matchMargin inflate={50} />
⋮⋮
{ let items = store .get(m.items) .filter((item) => item.id !== source.data.id); store.set(m.items, [...items, source.data]); }} matchWidth matchHeight matchMargin inflate={50} >
); ``` ## Usage To use `DragHandle`, set `handled` on the parent `DragSource`: ```tsx
⋮⋮
``` Without `handled`, the entire `DragSource` content would be draggable, preventing text selection and other interactions. ## Configuration | Property | Type | Description | | ----------- | -------- | ---------------------------------------------------------------- | | `style` | `Style` | Inline styles for the handle element. | | `class` | `Class` | CSS class for the handle element. | | `baseClass` | `string` | Base CSS class applied to the element. Defaults to `draghandle`. | --- # Keyboard Shortcuts ```ts import { registerKeyboardShortcut } from 'cx/ui'; ``` CxJS provides `registerKeyboardShortcut` for listening to keyboard shortcuts. Shortcuts only trigger when the document body has focus (not when focused on input fields). ## Example ```tsx import { createModel } from "cx/data"; import { Controller, registerKeyboardShortcut } from "cx/ui"; import { KeyCode } from "cx/util"; import { Checkbox } from "cx/widgets"; interface PageModel { enterPressed: boolean; shiftAPressed: boolean; } const m = createModel(); class PageController extends Controller { unregisterEnter!: () => void; unregisterShiftA!: () => void; onInit() { this.unregisterEnter = registerKeyboardShortcut(KeyCode.enter, () => { this.store.toggle(m.enterPressed); }); this.unregisterShiftA = registerKeyboardShortcut( { keyCode: KeyCode.a, shiftKey: true }, () => { this.store.toggle(m.shiftAPressed); }, ); } onDestroy() { this.unregisterEnter(); this.unregisterShiftA(); } } export default (
Click outside the checkboxes and press the shortcuts:
); ``` ## Key Argument The first argument can be a simple key code or an object for key combinations: ```tsx // Simple key registerKeyboardShortcut(KeyCode.enter, callback); // Key combination registerKeyboardShortcut({ keyCode: KeyCode.a, shiftKey: true }, callback); ``` The object form accepts: - `keyCode` (required) - A value from the `KeyCode` class - `shiftKey` - Require Shift key - `ctrlKey` - Require Ctrl key - `altKey` - Require Alt key ## KeyCode ```ts import { KeyCode } from 'cx/util'; ``` Common key codes: | Key | Code | | ------------ | ------------------------------------------------------------- | | Enter | `KeyCode.enter` | | Escape | `KeyCode.esc` | | Space | `KeyCode.space` | | Tab | `KeyCode.tab` | | Backspace | `KeyCode.backspace` | | Delete | `KeyCode.delete` | | Arrow keys | `KeyCode.up`, `KeyCode.down`, `KeyCode.left`, `KeyCode.right` | | Page Up/Down | `KeyCode.pageUp`, `KeyCode.pageDown` | | Home/End | `KeyCode.home`, `KeyCode.end` | | Letter A | `KeyCode.a` | Always unregister keyboard shortcuts when they're no longer needed to prevent memory leaks. Call the unregister function returned by `registerKeyboardShortcut` in the controller's `onDestroy` method. --- # Localization CxJS supports culture-specific number, currency, and date formatting based on `Intl` helpers provided by modern browsers. It also offers translation of standard widget messages to different languages. ## Example ```tsx import { createModel } from "cx/data"; import { ContentResolver, Controller, Culture, LabelsTopLayout } from "cx/ui"; import { Calendar, LookupField, NumberField, enableTooltips } from "cx/widgets"; enableTooltips(); interface Option { id: string; text: string; } interface PageModel { number: number; date: string; culture: Option; cultures: Option[]; currency: Option; currencies: Option[]; } const m = createModel(); class PageController extends Controller { onInit() { this.store.set(m.number, 123456.78); this.store.set(m.date, new Date().toISOString()); this.store.set(m.cultures, [ { id: "en-us", text: "English (US)" }, { id: "de-de", text: "German" }, { id: "es-es", text: "Spanish" }, { id: "fr-fr", text: "French" }, ]); this.store.set(m.culture, { id: "en-us", text: "English (US)" }); this.store.set(m.currencies, [ { id: "USD", text: "USD" }, { id: "EUR", text: "EUR" }, { id: "GBP", text: "GBP" }, ]); this.store.set(m.currency, { id: "USD", text: "USD" }); } } export default (
{ //console.log(culture, currency); Culture.setCulture(culture); Culture.setDefaultCurrency(currency); switch (culture) { case "de-de": await import("cx/locale/de-de.js"); break; case "es-es": await import("cx/locale/es-es.js"); break; case "fr-fr": await import("cx/locale/fr-fr.js"); break; default: await import("cx/locale/en-us.js"); break; } return ( ); }} />
); ``` ## Culture ```ts import { Culture } from 'cx/ui'; ``` The `Culture` object provides methods for selecting UI cultures used for formatting. ### Setting Culture ```tsx import "cx/locale/de-de"; // Import locale data Culture.setCulture("de-de"); ``` Available locale packages include: `de-de`, `en-us`, `es-es`, `fr-fr`, `nl-nl`, `pt-pt`, `sr-latn-ba`. ### Culture Methods | Method | Description | | ------------------------------------ | --------------------------------------------------- | | `Culture.setCulture(code)` | Sets the current culture for all formatting. | | `Culture.setNumberCulture(code)` | Sets culture for number formatting only. | | `Culture.setDateTimeCulture(code)` | Sets culture for date/time formatting only. | | `Culture.setDefaultCurrency(code)` | Sets the default currency (e.g., `"EUR"`, `"USD"`). | | `Culture.setDefaultTimezone(tz)` | Sets the default timezone. | | `Culture.setDefaultDateEncoding(fn)` | Sets function for encoding dates. | ### Dynamic Culture Loading Use dynamic imports for code-splitting: ```tsx async function loadCulture(culture: string) { switch (culture) { case "de-de": await import("cx/locale/de-de"); break; case "fr-fr": await import("cx/locale/fr-fr"); break; default: await import("cx/locale/en-us"); } Culture.setCulture(culture); store.notify(); // Force re-render } ``` ## Localization ```ts import { Localization } from 'cx/ui'; ``` The `Localization` object provides methods for translating widget messages. ### Localization Methods | Method | Description | | ---------------------------------------------- | ----------------------------------------------- | | `Localization.localize(culture, name, values)` | Override widget properties for a given culture. | | `Localization.override(name, values)` | Override widget properties for all cultures. | | `Localization.registerPrototype(name, type)` | Register a component type for localization. | ### Translating Widget Messages ```tsx Localization.localize("de", "cx/widgets/TextField", { validationErrorText: "Ungültige Eingabe.", minLengthValidationErrorText: "Mindestens {0} Zeichen erforderlich.", maxLengthValidationErrorText: "Maximal {0} Zeichen erlaubt.", }); ``` ### Overriding Defaults Use `override` to change defaults for all cultures: ```tsx Localization.override("cx/widgets/DateField", { format: "dd.MM.yyyy", }); ``` --- # DetachedScope ```ts import { DetachedScope } from 'cx/widgets'; ``` For most performance optimization scenarios, consider using [PrivateStore](/docs/concepts/private-store) instead, which is more commonly used and easier to work with. `DetachedScope` improves performance by detaching certain areas from the main render loop. Detached contents render in their own render loop and use a data declaration to specify which changes can flow in or out. Use this for heavy components like grids, charts, or rich popups that might be negatively affected by other elements on the page. ```tsx import { createModel } from "cx/data"; import { DetachedScope } from "cx/widgets"; interface Model { scope1: { count: number }; scope2: { count: number }; } const m = createModel(); export const model = { scope1: { count: 0 }, scope2: { count: 0 }, }; export default (

Detached Scope 1

Detached Scope 2

); ``` ## How It Works 1. The `bind` prop specifies the data paths this scope depends on 2. The scope renders independently from the rest of the page 3. Only changes to the bound data trigger re-renders inside the scope 4. Changes inside the scope don't trigger re-renders outside ## Configuration | Property | Type | Description | | -------- | ---- | ----------- | | `bind` | `string \| array` | Binding path(s) to monitor for changes. Shorthand for `data`. | | `data` | `object` | Data selector object. Children update only when this data changes. | | `exclusive` | `string \| array` | Paths for exclusive data that only affects this scope. | | `exclusiveData` | `object` | Exclusive data selector for data used only within this scope. | | `name` | `string` | Name of the scope for debugging purposes. | --- # IsolatedScope ```ts import { IsolatedScope } from 'cx/widgets'; ``` For most performance optimization scenarios, consider using [PrivateStore](/docs/concepts/private-store) instead, which is more commonly used and easier to work with. `IsolatedScope` improves performance by isolating certain areas from unnecessary recomputations. Contents of an isolated scope will only update when the specified data changes. Use this for grids, charts, or any rich content that might cause performance issues for the rest of the page. ```tsx import { createModel } from "cx/data"; import { IsolatedScope } from "cx/widgets"; interface Model { scope1: { count: number }; scope2: { count: number }; } const m = createModel(); export const model = { scope1: { count: 0 }, scope2: { count: 0 }, }; export default (

Isolated Scope 1

Isolated Scope 2

); ``` ## How It Works 1. The `bind` prop specifies the data paths this scope depends on 2. Children only re-render when the bound data changes 3. Changes to other store data do not trigger re-renders inside the scope ## IsolatedScope vs DetachedScope - **IsolatedScope** - Prevents unnecessary re-renders by filtering which data changes affect the children - **DetachedScope** - Goes further by running in a completely separate render loop, providing stronger isolation Use `IsolatedScope` for simpler cases. Use `DetachedScope` when you need maximum isolation. ## Configuration | Property | Type | Description | | -------- | ---- | ----------- | | `bind` | `string \| array` | Binding path(s) to monitor for changes. Shorthand for `data`. | | `data` | `object` | Data selector object. Children update only when this data changes. | --- # Theming Overview CxJS uses a layered SCSS architecture that makes every visual aspect of the framework customizable. Understanding the key building blocks — CSS class naming, state style maps, and the variable/map/override pipeline — will help you get the most out of any theme. ## BESM Class Naming CxJS uses a BEM-like naming convention called **BESM** (Block, Element, State, Modifier) with short prefixes: | Prefix | Role | Example | Meaning | | ------ | -------- | ------------------ | -------------------------------- | | `cxb-` | Block | `cxb-button` | Top-level component element | | `cxe-` | Element | `cxe-button-icon` | Part of a block component | | `cxs-` | State | `cxs-disabled` | State that affects appearance | | `cxm-` | Modifier | `cxm-primary` | User-supplied variant | A primary button in its disabled state would have the classes `cxb-button cxm-primary cxs-disabled`. ## State Style Maps Every widget's appearance is defined by a **state style map** — an SCSS map that holds CSS properties for each visual state (default, hover, focus, active, disabled, etc.): ```scss $cx-button-state-style-map: ( default: ( background-color: #eee, color: #333, border-width: 1px, ), hover: ( box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), ), focus: ( outline: none, box-shadow: 0 0 1px 1px rgba(77, 144, 254, 0.8), ), disabled: ( color: gray, pointer-events: none, ), ) !default; ``` The framework compiles these maps into CSS rules using mixins that iterate over the map entries and emit the corresponding properties for each pseudo-class and modifier combination. Because state style maps are just data, themes and applications can **merge** additional properties into them without replacing the whole map. ## Modifiers Widgets also define **modifier maps** for variants like `primary`, `danger`, or `hollow`. Each modifier has its own set of state overrides: ```scss $cx-button-mods: ( primary: ( default: (background-color: #1f99f8, color: white), hover: (background-color: darken(#1f99f8, 5%)), ), danger: ( default: (background-color: #d32f2f, color: white), ), ) !default; ``` These get compiled into CSS rules like `.cxb-button.cxm-primary { ... }` and `.cxb-button.cxm-primary:hover { ... }`. ## Three-Layer Customization Themes and applications customize the framework at three levels: ### 1. Variables Variables control base values — colors, spacing, border radius, font sizes. They are defined with `!default` so that outer values take precedence: ```scss $cx-default-button-background-color: #eee !default; $cx-default-button-border-width: 1px !default; ``` Themes override variables using `@forward...with()`, which sets the values before the framework sees them: ```scss @forward "cx/src/variables" with ( $cx-default-button-background-color: #f6f6f6 !default, $cx-default-button-border-width: 0 !default ); ``` ### 2. Maps After variables are resolved, state style maps are constructed. Themes use `cx-deep-map-merge()` to overlay their changes without replacing the whole map: ```scss @use "cx/src/util/scss/deep-merge" as *; $cx-button-state-style-map: cx-deep-map-merge( $cx-button-state-style-map, ( default: (transition: all 0.15s ease), hover: (box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5)), ) ); ``` Only the specified keys are overridden — all other states and properties remain intact. ### 3. CSS Overrides For anything that doesn't fit into variables or maps, plain CSS rules can be added after the theme is loaded: ```scss .cxb-button.cxm-primary { text-transform: uppercase; } ``` ## How a Theme Plugs In A typical theme package has this structure: ``` cx-theme-aquamarine/src/ ├── index.scss — Entry point (controls load order) ├── variables.scss — Overrides CxJS variable defaults ├── maps.scss — Merges into state style maps └── overrides.scss — Additional CSS rules ``` The load order matters: 1. **Variables** are forwarded first, setting values before the framework reads them. 2. **Maps** are loaded next, merging theme-specific state overrides into the framework maps. 3. **The framework SCSS** compiles, using the now-customized variables and maps to generate CSS. 4. **Overrides** add any final CSS rules on top. When an application uses a theme, it imports the theme's entry point, and the SCSS compiler resolves everything in a single pass. ## Next Steps - [CSS Variables Theme](/docs/concepts/css-variables-theme) — runtime theming with `cx-theme-variables` - [Classic Themes](/docs/concepts/classic-themes) — compile-time SCSS themes and customization --- # CSS Variables Theme The [`cx-theme-variables`](https://www.npmjs.com/package/cx-theme-variables) theme uses CSS custom properties, enabling runtime theme switching (e.g., light/dark mode) without recompiling SCSS. Preview it in the [Theme Editor](/themes). ## Installation ```bash npm install cx-theme-variables ``` ## Usage Import the CSS files in your application: ```tsx import "cx-theme-variables/dist/reset.css"; import "cx-theme-variables/dist/widgets.css"; import "cx-theme-variables/dist/charts.css"; import "cx-theme-variables/dist/svg.css"; ``` Or import from SCSS source: ```scss @use "cx-theme-variables/src/index"; ``` ## Customization with Presets The preferred way to customize this theme is through JavaScript using `renderThemeVariables` with presets and tweaks: ```tsx import { renderThemeVariables, defaultPreset, roundingLarge, densityCompact, fontInter, } from "cx-theme-variables"; renderThemeVariables({ ...defaultPreset, ...roundingLarge, ...densityCompact, ...fontInter, }); ``` Available presets: `defaultPreset`, `darkBluePreset`, `darkGrayPreset`, `oceanPreset`. Available tweaks: - **Rounding**: `roundingNone`, `roundingSmall`, `roundingMedium`, `roundingLarge`, `roundingVeryLarge` - **Density**: `densityMinimal`, `densityCondensed`, `densityCompact`, `densityNormal`, `densityComfortable`, `densitySpacious` - **Font**: `fontSystem`, `fontInter`, `fontRoboto`, `fontOpenSans`, `fontPoppins`, `fontLato` ## Dark Mode Use the `cssWrapper` parameter to scope a preset to a media query: ```tsx import { renderThemeVariables, defaultPreset, darkBluePreset, } from "cx-theme-variables"; // Light theme by default renderThemeVariables(defaultPreset); // Dark theme when user prefers dark mode renderThemeVariables( darkBluePreset, ":root", "@media (prefers-color-scheme: dark)", ); ``` ## Dynamic Themes with ThemeVarsRoot For dynamic theme changes driven by application state, use the `ThemeVarsRoot` CxJS widget instead of `renderThemeVariables`. It renders a `