diff --git a/packages/ctablex-core/README.md b/packages/ctablex-core/README.md new file mode 100644 index 0000000..cf637b9 --- /dev/null +++ b/packages/ctablex-core/README.md @@ -0,0 +1,219 @@ +# @ctablex/core + +Core building blocks for composable, context-based React components using the **micro-context pattern**. + +## What is Micro-Context? + +**Micro-context** is a pattern for passing data through localized React Context instead of props. Unlike traditional context patterns that span entire applications, micro-context creates small, scoped providers within component subtrees for fine-grained data flow. + +This enables: + +- **Reusable components** that work anywhere without knowing the data source +- **Flexible composition** of data transformers and renderers +- **No prop drilling** through intermediate components +- **Immutable children** for better performance optimization + +Read more: [Micro-Context Pattern](./docs/MICRO-CONTEXT.md) + +## Installation + +```bash +npm install @ctablex/core +``` + +## Quick Start + +```tsx +import { ContentProvider, FieldContent, DefaultContent } from '@ctablex/core'; + +type User = { + name: string; + email: string; +}; + +const user: User = { + name: 'Alice', + email: 'alice@example.com', +}; + +// Provide data via context + + {/* Access fields without props */} + + + +; +// Renders: Alice +``` + +## Core Concepts + +### ContentProvider & useContent + +The foundation of micro-context: + +```tsx +import { ContentProvider, useContent } from '@ctablex/core'; + +// Provide data + + +; + +// Consume data +function MyComponent() { + const data = useContent(); + return
{data}
; +} +``` + +Read more: [ContentContext - ContentProvider & useContent](./docs/ContentContext.md) + +### Content Components + +Transform and access data through context: + +```tsx +import { + ContentValue, + FieldContent, + ArrayContent, + ObjectContent, + NullableContent, + DefaultContent, + KeyContent, +} from '@ctablex/core'; + +// Access nested paths + + + + +// Access object fields + + + + +// Iterate arrays + + + + + + + + +// Iterate objects + + + : + + + +// Handle null/undefined + + + + + +``` + +Read more: [Contents](./docs/Contents.md), [ArrayContent](./docs/ArrayContent.md), [ObjectContent](./docs/ObjectContent.md) + +### Type-Safe Accessors + +Extract values with strong TypeScript support: + +```tsx +import { access } from '@ctablex/core'; + +type User = { + profile: { + name: string; + }; +}; + +const user: User = { profile: { name: 'Bob' } }; + +// Path accessor with autocomplete +const name = access(user, 'profile.name'); // ✓ Type-safe + autocomplete + +// Function accessor +const value = access(user, (u) => u.profile.name); // ✓ Type inference + auto type safety +``` + +Read more: [Accessors](./docs/Accessors.md) + +## Key Features + +### Reusable Renderers + +Components work anywhere without knowing the data source: + +```tsx +function PriceDisplay() { + const price = useContent(); + return ${price.toFixed(2)}; +} + +// Works in any context that provides a number + + +; +``` + +### Default Children and Open for Customization + +Components provide sensible defaults while remaining customizable: + +```tsx +// Simple - uses DefaultContent + + +// Custom rendering + + + +``` + +### Performance Optimization + +Immutable children enable powerful memoization: + +```tsx +const content = ( + + + + + +); + +function ProductList() { + return content; // Same reference every render +} +``` + +## Type Safety Limitations + +⚠️ Micro-context provides weak type safety. Generic types must be manually specified and cannot be validated across context boundaries. See [MICRO-CONTEXT.md - Weak Type Safety](./docs/MICRO-CONTEXT.md#weak-type-safety) for details. + +## Documentation + +For detailed documentation, common patterns, and pitfalls, see: + +- **[Documentation Guide](./docs/README.md)** - Start here for a complete overview +- **[Micro-Context Pattern](./docs/MICRO-CONTEXT.md)** - Understand the core concept +- **[ContentContext](./docs/ContentContext.md)** - ContentProvider, useContent, ContentContext +- **[Contents](./docs/Contents.md)** - ContentValue, FieldContent, NullableContent, DefaultContent +- **[ArrayContent](./docs/ArrayContent.md)** - Array iteration components +- **[ObjectContent](./docs/ObjectContent.md)** - Object iteration components +- **[Accessors](./docs/Accessors.md)** - Type-safe value extraction + +## License + +MIT + +## Related Packages + +- **[@ctablex/table](https://www.npmjs.com/package/@ctablex/table)** - Composable table components built on @ctablex/core diff --git a/packages/ctablex-core/docs/Accessors.md b/packages/ctablex-core/docs/Accessors.md new file mode 100644 index 0000000..1466ac5 --- /dev/null +++ b/packages/ctablex-core/docs/Accessors.md @@ -0,0 +1,550 @@ +# Accessors + +Accessors are functions that extract values from data structures with **strong TypeScript support**. Unlike the weak type safety of generic context parameters, accessors provide **autocomplete and compile-time error detection**. + +**Note:** Most of the time, users don't directly interact with accessors. These types are used internally by the library (e.g., in `ContentValue` or `getKey` props). + +## Overview + +The accessor system provides three types of accessors: + +- **Path Accessors** - String-based paths like `"user.address.city"` +- **Function Accessors** - Custom extraction functions like `(user) => user.fullName` +- **Unified Accessors** - Accept either path strings, functions, `undefined`, or `null` + +## Path Accessors + +### accessByPath + +Accesses nested properties using a dot-separated string path with **full type safety and autocomplete**. + +```tsx +function accessByPath>( + t: T, + path: K, +): PathAccessorValue; +``` + +#### Example + +```tsx +import { accessByPath } from '@ctablex/core'; + +type User = { + name: string; + address: { + city: string; + zip: number; + }; +}; + +const user: User = { + name: 'John', + address: { city: 'NYC', zip: 10001 }, +}; + +// ✓ Type-safe with autocomplete +const city = accessByPath(user, 'address.city'); // string +const zip = accessByPath(user, 'address.zip'); // number + +// ✓ Compile-time error for invalid paths +const invalid = accessByPath(user, 'address.country'); // ✗ Error! +const typo = accessByPath(user, 'addres.city'); // ✗ Error! +``` + +#### Type Safety + +- **Autocomplete** - IDE suggests valid paths: `"name"`, `"address"`, `"address.city"`, `"address.zip"` +- **Compile-time errors** - Invalid paths are caught during development +- **Return type inference** - TypeScript knows `accessByPath(user, 'address.city')` returns `string` + +### accessByPathTo + +Like `accessByPath`, but constrains paths to those that return a specific type. + +```tsx +function accessByPathTo>( + t: T, + path: K, +): R & PathAccessorValue; +``` + +#### Example + +```tsx +type Product = { + name: string; + price: number; + inStock: boolean; + metadata: { + weight: number; + sku: string; + }; +}; + +const product: Product = { + name: 'Widget', + price: 99.99, + inStock: true, + metadata: { weight: 1.5, sku: 'WDG-001' }, +}; + +// ✓ Explicit type arguments (R, T) +const price1 = accessByPathTo(product, 'price'); // ✓ +const weight1 = accessByPathTo(product, 'metadata.weight'); // ✓ + +// ✓ Type inference from variable annotation +const price2: number = accessByPathTo(product, 'price'); // ✓ +const weight2: number = accessByPathTo(product, 'metadata.weight'); // ✓ + +// ✗ Compile error - these paths don't return numbers +const name: number = accessByPathTo(product, 'name'); // ✗ Error! 'name' is not a number path +const stock: number = accessByPathTo(product, 'inStock'); // ✗ Error! 'inStock' is not a number path + +// ✗ Type mismatch - path returns string, not number +const sku: number = accessByPath(product, 'metadata.sku'); // ✗ Error! Type 'string' is not assignable to 'number' +``` + +**Use case:** Ensuring extracted values match an expected type, useful for components that require specific value types. + +### PathAccessor Type + +The string literal type representing valid paths through an object structure. + +```tsx +type PathAccessor = /* ... */; +``` + +Supports: + +- Object properties: `"user"`, `"address"` +- Nested paths: `"user.address.city"` (up to 5 levels deep) +- Array indices for tuples: `tuple.0`, `tuple.1` + +### PathAccessorValue Type + +The type of the value at a given path in an object. + +```tsx +type PathAccessorValue = /* ... */; +``` + +Computes the type of the value accessed by a path. Used internally by `accessByPath` to infer return types. + +#### Example + +```tsx +type User = { + name: string; + address: { + city: string; + zip: number; + }; +}; + +type Name = PathAccessorValue; // string +type City = PathAccessorValue; // string +type Zip = PathAccessorValue; // number +``` + +### PathAccessorTo Type + +The string literal type representing paths that return a specific type. + +```tsx +type PathAccessorTo = /* ... */; +``` + +Filters `PathAccessor` to only include paths where `PathAccessorValue` extends `R`. + +#### Example + +```tsx +type Product = { + name: string; + price: number; + metadata: { + weight: number; + sku: string; + }; +}; + +// Only paths that return numbers +type NumberPaths = PathAccessorTo; +// Result: "price" | "metadata.weight" + +// Only paths that return strings +type StringPaths = PathAccessorTo; +// Result: "name" | "metadata.sku" +``` + +## Function Accessors + +### accessByFn + +Accesses values using a custom function with type inference. + +```tsx +function accessByFn>( + obj: T, + fn: F, +): FnAccessorValue; +``` + +**Note:** This function is rarely used directly. Use the unified `access` function instead. + +#### Example + +```tsx +type User = { + firstName: string; + lastName: string; +}; + +const user: User = { + firstName: 'John', + lastName: 'Doe', +}; + +// Custom extraction +const fullName = accessByFn(user, (u) => `${u.firstName} ${u.lastName}`); +// Returns: "John Doe" + +// Complex logic +const isJohn = accessByFn(user, (u) => u.firstName === 'John'); +// Returns: true +``` + +### FnAccessor Type + +A function that extracts a value from an object. + +```tsx +type FnAccessor = (t: T) => R; +``` + +### FnAccessorValue Type + +The return type of a function accessor. + +```tsx +type FnAccessorValue> = /* ... */; +``` + +Infers the return type of a function accessor. Used internally by `accessByFn` to determine return types. + +#### Example + +```tsx +type User = { + firstName: string; + lastName: string; +}; + +type FullNameFn = (u: User) => string; +type FullNameValue = FnAccessorValue; // string + +type IsAdultFn = (u: User) => boolean; +type IsAdultValue = FnAccessorValue; // boolean +``` + +## Unified Accessors + +### access + +The main accessor function that accepts **path strings, functions, `undefined`, or `null`**. + +```tsx +function access>(t: T, a: A): AccessorValue; +``` + +#### Behavior + +- **`undefined`** - Returns the input value unchanged +- **`null`** - Returns `null` +- **String** - Uses `accessByPath` +- **Function** - Calls the function with the input value + +#### Example + +```tsx +import { access } from '@ctablex/core'; + +type User = { + name: string; + age: number; +}; + +const user: User = { name: 'Alice', age: 30 }; + +// Path accessor +access(user, 'name'); // "Alice" + +// Function accessor +access(user, (u) => u.age > 18); // true + +// Undefined - returns input +access(user, undefined); // { name: 'Alice', age: 30 } + +// Null - returns null +access(user, null); // null +``` + +### accessTo + +Like `access`, but constrains to accessors that return a specific type. + +```tsx +function accessTo>( + t: T, + a: A, +): R & AccessorValue; +``` + +#### Example + +```tsx +type Product = { + name: string; + price: number; + tags: string[]; +}; + +const product: Product = { + name: 'Widget', + price: 99.99, + tags: ['electronics', 'gadget'], +}; + +// ✓ Explicit type arguments (R, T) +const price1 = accessTo(product, 'price'); // 99.99 +const doubled1 = accessTo(product, (p) => p.price * 2); // 199.98 + +// ✓ Type inference from variable annotation +const price2: number = accessTo(product, 'price'); // 99.99 +const doubled2: number = accessTo(product, (p) => p.price * 2); // 199.98 +const length: number = accessTo(product, (p) => p.tags.length); // 2 + +// ✗ Compile error - accessor doesn't return number +const name1: number = accessTo(product, 'name'); // ✗ Error! 'name' is not a number path +const tags: number = accessTo(product, (p) => p.tags); // ✗ Error! Returns string[], not number + +// ✗ Type mismatch - path returns string, not number +const name2: number = access(product, 'name'); // ✗ Error! Type 'string' is not assignable to 'number' +``` + +### Accessor Type + +Union type accepting all accessor types. + +```tsx +type Accessor = undefined | null | PathAccessor | FnAccessor; +``` + +### AccessorValue Type + +The type of the value returned by an accessor. + +```tsx +type AccessorValue> = A extends undefined + ? T + : A extends null + ? null + : A extends PathAccessor + ? PathAccessorValue + : A extends FnAccessor + ? FnAccessorValue + : never; +``` + +Computes the return type of the `access` function based on the accessor type: + +- `undefined` → returns `T` (the input) +- `null` → returns `null` +- Path string → returns `PathAccessorValue` +- Function → returns `FnAccessorValue` + +#### Example + +```tsx +type User = { + name: string; + age: number; +}; + +type Value1 = AccessorValue; // User +type Value2 = AccessorValue; // null +type Value3 = AccessorValue; // string +type Value4 = AccessorValue boolean>; // boolean +``` + +### AccessorTo Type + +Union type accepting accessors that return a specific type. + +```tsx +type AccessorTo = + | undefined + | null + | PathAccessorTo + | FnAccessor; +``` + +Like `Accessor`, but constrains path accessors to those that return type `R`, and function accessors to those with return type `R`. + +#### Example + +```tsx +type Product = { + name: string; + price: number; + metadata: { + weight: number; + sku: string; + }; +}; + +// Accepts any accessor that returns a number +type NumberAccessor = AccessorTo; + +// Valid number accessors: +const accessor1: NumberAccessor = 'price'; // ✓ Path to number +const accessor2: NumberAccessor = 'metadata.weight'; // ✓ Path to number +const accessor3: NumberAccessor = (p) => p.price * 2; // ✓ Function returning number +const accessor4: NumberAccessor = undefined; // ✓ Returns Product (any) +const accessor5: NumberAccessor = null; // ✓ Returns null + +// Invalid - don't return numbers: +const accessor6: NumberAccessor = 'name'; // ✗ Error! Path to string +const accessor7: NumberAccessor = (p) => p.name; // ✗ Error! Function returns string +``` + +## Type Safety Advantages + +Accessors have **strong type safety** compared to generic context parameters: + +### Path Accessors + +```tsx +type User = { + address: { + city: string; + }; +}; + +// ✓ Autocomplete suggests: "address", "address.city" +accessByPath(user, 'address.city'); + +// ✗ Compile-time error +accessByPath(user, 'address.country'); // Property 'country' does not exist +accessByPath(user, 'addres.city'); // Property 'addres' does not exist +``` + +### Function Accessors + +```tsx +// ✓ Full type inference +access(user, (u) => u.address.city); // TypeScript knows return type is string + +// ✓ Autocomplete works inside function +access(user, (u) => u./* autocomplete shows: address */); +``` + +### Compared to Generic Context + +Accessors provide validation **based on the provided generic type**, while context generics themselves are not validated: + +```tsx +type User = { + address: { + city: string; + }; +}; + +// ✗ Weak type safety - generic type itself is not validated +const city = useContent(); // TypeScript cannot verify User matches actual context value + +// ✓ Strong type safety - accessor IS validated based on generic type + accessor="address.city"> {/* ✓ TypeScript validates "address.city" exists on User */} + + + + accessor="address.country"> {/* ✗ Error! "country" doesn't exist on User */} + + +``` + +**Key difference:** + +- Generic type `` on context is not validated by TypeScript (you could pass wrong type) +- But **given** that generic type, the `accessor` prop is fully validated with autocomplete +- This provides partial type safety: accessors are validated, but the generic type itself is not + +## Limitations + +### Path Depth + +Path accessors support up to **5 levels of nesting**: + +```tsx +// ✓ Supported +'a.b.c.d.e'; + +// ✗ Not supported +'a.b.c.d.e.f'; // Too deep +``` + +### Arrays + +Path accessors work with **tuple types** but not generic arrays: + +```tsx +type Tuple = [string, number]; +const tuple: Tuple = ['hello', 42]; + +// ✓ Works with tuples +accessByPath(tuple, '0'); // "hello" +accessByPath(tuple, '1'); // 42 + +// ✗ Generic arrays require function accessors +type StringArray = string[]; +const arr: StringArray = ['a', 'b', 'c']; +access(arr, (a) => a[0]); // Use function instead +``` + +## Usage with Content Components + +Accessors are commonly used with `ContentValue` and other content components: + +```tsx +import { ContentValue, FieldContent } from '@ctablex/core'; + +type User = { + profile: { + name: string; + age: number; + }; +}; + +// Path accessor + accessor="profile.name"> + + + +// Function accessor + accessor={(user) => user.profile.age > 18}> + + + +// Undefined accessor (returns whole object) + accessor={undefined}> + ... + +``` + +## Related + +- [ContentValue](./Contents.md#contentvalue) - Component using accessors +- [FieldContent](./Contents.md#fieldcontent) - Simplified object field access +- [Content Context](./ContentContext.md) - Context system foundation +- [Micro-Context Pattern](./MICRO-CONTEXT.md) - Pattern overview diff --git a/packages/ctablex-core/docs/ArrayContent.md b/packages/ctablex-core/docs/ArrayContent.md new file mode 100644 index 0000000..737b970 --- /dev/null +++ b/packages/ctablex-core/docs/ArrayContent.md @@ -0,0 +1,468 @@ +# Array Content Components + +Components for iterating over arrays and accessing array indices. + +## TL;DR + +- Use `` to iterate over arrays +- Use `` to display the current array index +- Use `IndexContext`, `useIndex` to access the current index +- Use `` to render content when the array is empty +- Use `` to render content when the array is not empty + +## ArrayContent + +Iterates over an array, rendering children for each element. Provides both the array element and its index via context. + +### Props + +```tsx +interface ArrayContentProps { + getKey?: PathAccessorTo | ArrayGetKey; + children?: ReactNode; + join?: ReactNode; + value?: ReadonlyArray; +} + +type ArrayGetKey = (value: V, index: number) => string | number; +``` + +- **`getKey`** - Extracts a unique key from each element (optional, defaults to index) + - Path string: `"id"` extracts the `id` property + - Function: `(value, index) => value.id` for custom logic +- **`children`** - Content to render for each element (defaults to ``) +- **`join`** - Content to render between elements (e.g., commas, separators) +- **`value`** - Array to iterate (optional, uses context value if omitted) + +### Behavior + +- Iterates over the array from the content context +- Provides the array index via `IndexContext` +- Provides each element via `ContentProvider` +- Renders `join` content between elements (not before the first element) +- Uses `getKey` to generate React keys for list items + +### Example + +```tsx +import { ArrayContent, IndexContent } from '@ctablex/core'; + +type User = { + id: number; + name: string; +}; + +const users: User[] = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, +]; + +// Basic iteration + + + + + + + +// Renders: AliceBob + +// With separator + + + + + + + +// Renders: Alice, Bob + +// With index + + + . + + +// Renders: 1. Alice2. Bob + +// Custom key function + + `user-${user.id}`}> + +
+ +
+
+
+
+// Renders:
Alice
Bob
+``` + +### Type Safety + +The generic type `V` on `ArrayContent` provides type safety for the `getKey` prop: + +```tsx +// ✓ Type-safe getKey with autocomplete + getKey="id"> {/* ✓ Autocomplete suggests "id", "name" */} +
...
+
+ +// ✗ Compile error - invalid path + getKey="email"> {/* ✗ Error! "email" doesn't exist on User */} +
...
+ + +// ✓ Type-safe function + getKey={(user, index) => user.id}> {/* ✓ "user" is typed as User */} +
...
+ +``` + +However, **nested components are not automatically type-checked**: + +```tsx +// ✗ No type checking - nested FieldContent doesn't inherit User type +> + {/* No error, but no autocomplete either */} + + + + +// ✓ Type-safe with explicit generic on FieldContent +> + field="name"> {/* ✓ "name" exists on User - autocomplete works! */} + +
+ + +// ✗ Compile error with explicit generic +> + field="email"> {/* ✗ Error! "email" doesn't exist on User */} + + + +``` + +**Summary:** + +- `ArrayContent` generic provides type safety for `getKey` prop +- Nested components need their own explicit generic like `>` for type checking + +## IndexContent + +Displays the current array index from `IndexContext`. + +### Props + +```tsx +interface IndexContentProps { + start?: number; +} +``` + +- **`start`** - Offset to add to the index (optional, defaults to 0) + +### Behavior + +- Retrieves the current index from `IndexContext` +- Adds the `start` offset to the index +- Renders the resulting number + +### Example + +```tsx +const items = ['Apple', 'Banana', 'Cherry']; + +// Zero-based index + + + : + + +// Renders: 0: Apple1: Banana2: Cherry + +// One-based index + + + . + + +// Renders: 1. Apple2. Banana3. Cherry +``` + +### Error Handling + +Throws an error if used outside `IndexContext`: + +```tsx +// ✗ Error: useIndex must be used within a IndexContext + +``` + +## IndexContext + +React Context providing the current array index. + +```tsx +const IndexContext: React.Context; +function useIndex(): number; +``` + +### Behavior + +- Automatically provided by `ArrayContent` and `ObjectContent` +- Contains the current iteration index +- `useIndex()` throws if context is undefined + +### Example + +```tsx +import { useIndex } from '@ctablex/core'; + +function CustomIndexDisplay() { + const index = useIndex(); + return Item #{index + 1}; +} + + + + - + +; +// Renders: Item #1 - AItem #2 - BItem #3 - C +``` + +## EmptyContent + +Conditionally renders children when the content value is `null`, `undefined`, or empty (by default, empty arrays). + +### Props + +```tsx +interface EmptyContentProps { + children?: ReactNode; + isEmpty?: (content: C) => boolean; +} +``` + +- **`children`** - Content to render when value is empty +- **`isEmpty`** - Custom function to determine if content is empty (defaults to checking for empty arrays) + +### Behavior + +- Retrieves the content value from context +- Renders children if value is `null`, `undefined`, or satisfies the `isEmpty` predicate +- Returns `null` otherwise (renders nothing) +- Default `isEmpty` function: `Array.isArray(content) && content.length === 0` + +### Example + +```tsx +import { EmptyContent, ContentProvider } from '@ctablex/core'; + +// Renders for null + + +
No items
+
+
+// Renders:
No items
+ +// Renders for undefined + + +
No items
+
+
+// Renders:
No items
+ +// Renders for empty array (default behavior) + + +
No items
+
+
+// Renders:
No items
+ +// Renders nothing for non-empty array + + +
No items
+
+
+// Renders: (nothing) + +// Custom isEmpty predicate + + s.length === 0}> +
String is empty
+
+
+// Renders:
String is empty
+``` + +### Custom isEmpty Function + +Define custom logic to determine when content is considered empty: + +```tsx +type Product = { + name: string; + items: string[]; +}; + +// Custom isEmpty for objects + + p.items.length === 0}> +
No products in inventory
+
+
+ +// Custom isEmpty for strings + + s.trim().length === 0}> +
String is blank
+
+
+``` + +### Combining with NullableContent + +To show different content for `null`/`undefined` versus empty arrays, combine `NullableContent` and `EmptyContent`: + +```tsx + + Empty + + +``` + +## NonEmptyContent + +Conditionally renders children when the content value is **not** `null`, `undefined`, or empty. Inverse of `EmptyContent`. + +### Props + +```tsx +interface NonEmptyContentProps { + children?: ReactNode; + isEmpty?: (content: C) => boolean; +} +``` + +- **`children`** - Content to render when value is not empty +- **`isEmpty`** - Custom function to determine if content is empty (defaults to checking for empty arrays) + +### Behavior + +- Retrieves the content value from context +- Renders `null` (nothing) if value is `null`, `undefined`, or satisfies the `isEmpty` predicate +- Renders children otherwise +- Default `isEmpty` function: `Array.isArray(content) && content.length === 0` + +### Example + +```tsx +import { NonEmptyContent, ContentProvider } from '@ctablex/core'; + +// Renders nothing for null + + +
Has data
+
+
+// Renders: (nothing) + +// Renders nothing for empty array + + +
Has items
+
+
+// Renders: (nothing) + +// Renders children for non-empty array + + +
Has items
+
+
+// Renders:
Has items
+ +// Custom isEmpty with objects + + obj.items.length === 0}> +
Inventory has products
+
+
+// Renders:
Inventory has products
+``` + +### Use Cases + +- Display content only when data is available +- Wrap arrays with DOM elements only when not empty +- Apply custom `isEmpty` logic for different data types + +#### Wrapping Arrays with DOM Elements + +`NonEmptyContent` is especially useful when you want to wrap array content with DOM elements only when the array is not empty: + +```tsx +type Order = { + id: string; + items: string[]; +}; + +const order: Order = { + id: '123', + items: ['Widget', 'Gadget'], +}; + + + + +
    + +
  • + +
  • +
    +
+
+ +
Order is empty
+
+
+
; +// Renders:
  • Widget
  • Gadget
+// with empty items, renders:
Order is empty
+``` + +### Combining Empty and NonEmpty + +Use both components together to handle all cases: + +```tsx + + +
No results found
+
+ +

Results:

+ +
+ +
+
+
+
+``` + +## Related + +- [ObjectContent](./ObjectContent.md) - Iterate over object properties +- [ContentContext](./ContentContext.md) - Content value context +- [Accessors](./Accessors.md) - Type-safe value extraction +- [Micro-Context Pattern](./MICRO-CONTEXT.md) - Pattern overview diff --git a/packages/ctablex-core/docs/ContentContext.md b/packages/ctablex-core/docs/ContentContext.md new file mode 100644 index 0000000..abca0e8 --- /dev/null +++ b/packages/ctablex-core/docs/ContentContext.md @@ -0,0 +1,391 @@ +# Content Context + +The Content Context system provides the foundation for the micro-context pattern, enabling data flow through React Context instead of props. It consists of three parts that work together: + +- **`ContentContext`** - The underlying React Context +- **`ContentProvider`** - Component to provide values via context +- **`useContent`** - Hook to consume values from context + +## TL;DR + +Use `` to provide any value and make it available to descendant components. Use `useContent` within those components to access the provided value. `useContent` can also accept an optional override value. + +## Basic Usage + +```tsx +import { ContentProvider, useContent } from '@ctablex/core'; + +function App() { + const user = { name: 'John', age: 30 }; + + return ( + + + + ); +} + +function UserDisplay() { + const user = useContent<{ name: string; age: number }>(); + return ( +
+ {user.name} is {user.age} years old +
+ ); +} +``` + +## ContentProvider + +Wraps any value in a React Context, making it available to descendant components. + +### Props + +```tsx +interface ContentProviderProps { + value: V; + children?: ReactNode; +} +``` + +#### `value` + +**Type:** `V` (generic) + +The data to provide via context. Can be any type - primitives, objects, arrays, etc. + +```tsx + + + + + + + + + + + +``` + +#### `children` + +**Type:** `ReactNode` (optional) + +Components that will have access to the provided value via `useContent`. + +### Nesting + +Providers can be nested to create scoped contexts. Child providers override parent values: + +```tsx + + {/* accesses "outer" */} + + {/* accesses "inner" */} + + {/* accesses "outer" */} + +``` + +This nesting is the core of micro-context's data transformation pattern: + +```tsx + + {/* user context */} + + {/* address context */} + + {/* city context */} + + + + +``` + +### Performance + +`ContentProvider` uses `useMemo` internally to prevent unnecessary re-renders when the same value reference is provided. + +**Best practice:** Memoize or stabilize the `value` prop when possible: + +```tsx +// ✗ Creates new object every render - triggers context updates +function App() { + return ...; +} + +// ✓ Stable reference - no unnecessary updates +const user = { name: 'John' }; +function App() { + return ...; +} + +// ✓ Memoized when dependent on props/state +function App({ userId }) { + const user = useMemo(() => getUser(userId), [userId]); + return ...; +} +``` + +## useContent + +Retrieves the current value from the nearest `ContentProvider` in the component tree. + +### Signature + +```tsx +function useContent(value?: V): V; +``` + +### Type Parameter + +#### `V` + +The expected type of the content value. **This is purely manual** - TypeScript cannot verify it matches the actual context value. + +```tsx +// Type is a hint to TypeScript, not validated +const user = useContent(); // ✗ No compile-time validation + +// Wrong type compiles successfully +const user = useContent(); // ✗ No error! +``` + +### Parameter + +#### `value` (optional) + +**Type:** `V | undefined` + +An optional override value. If provided, this value is returned instead of the context value. + +```tsx +function Display({ value }: { value?: number }) { + const content = useContent(value); + return {content}; +} + +// Uses override value + + +// Uses context value + + {/* Displays: 99 */} + +``` + +**Use case:** Allows components to work both standalone (with props) and within context. + +### Error Handling + +`useContent` throws an error if called outside a `ContentProvider`: + +```tsx +function Component() { + const value = useContent(); // ✗ Error: useContent must be used within a ContentContext + return
{value}
; +} +``` + +**Exception:** If an override `value` parameter is provided, no error is thrown: + +```tsx +function Component() { + const value = useContent(42); // ✓ Returns 42, no context needed + return
{value}
; +} +``` + +## ContentContext + +The underlying React Context that powers `ContentProvider` and `useContent`. + +**⚠️ Internal API:** `ContentContext` is an internal implementation detail and may change in future versions. Always prefer using `ContentProvider` and `useContent` in application code. + +### Type + +```tsx +type ContentContextType = { value: V }; + +const ContentContext: React.Context | undefined>; +``` + +### When to Use Directly + +**Most applications should use `ContentProvider` and `useContent` instead.** Direct context usage is only needed for advanced scenarios: + +#### Optional Context Consumption + +When you want to handle missing context gracefully without throwing an error: + +```tsx +import { ContentContext } from '@ctablex/core'; +import { useContext } from 'react'; + +function OptionalDisplay() { + const context = useContext(ContentContext); + + if (!context) { + return
No data provided
; + } + + return
{context.value}
; +} +``` + +Compare with `useContent`, which throws an error when context is missing. + +#### Custom Context Logic + +When building your own abstractions: + +```tsx +function useContentOrDefault(defaultValue: V): V { + const context = useContext(ContentContext); + return context ? context.value : defaultValue; +} +``` + +#### Context Consumer Pattern + +Legacy consumer pattern instead of hooks: + +```tsx + + {(context) => (context ?
{context.value}
:
No context
)} +
+``` + +## Type Safety Limitations + +### No Type Inference or Validation + +The hook cannot infer the type from context, and TypeScript cannot verify that the type parameter matches the actual context value: + +```tsx +type User = { name: string }; + +function Component() { + // Must manually specify type - no inference + const user = useContent(); + return
{user.name}
; +} + +// ✓ Correct usage + + + + +// ✗ Type mismatch, but compiles! Runtime error! + + + +``` + +The problem is that `useContent()` accepts any type parameter, and there's no way for TypeScript to validate it against the actual context value at compile time. + +### Refactoring Risks + +Renaming fields won't automatically update all usages. Manual verification is required throughout the component tree. + +## Best Practices + +### Always Specify Type Parameter + +```tsx +// ✗ Avoid - type is `any` +const value = useContent(); + +// ✓ Better - explicit type +const value = useContent(); +``` + +### Use Close to Context Provider + +The further `useContent` is from its provider, the harder it is to verify type correctness: + +```tsx +// ✓ Easy to verify type + + {/* Uses useContent() */} + + +// ✗ Harder to track - many layers deep + + + + + {/* Uses useContent - what type? */} + + + + +``` + +### Consider Using Override Parameter + +For components that should work both with and without context: + +```tsx +function Display({ price }: { price?: number }) { + const value = useContent(price); + return ${value.toFixed(2)}; +} + +// Works standalone + + +// Works with context + + + +``` + +### Use High-Level APIs + +Prefer `ContentProvider` and `useContent` over direct `ContentContext` usage. They provide better error messages, simpler API, and built-in optimizations. + +## Examples + +### Simple Value Display + +```tsx +function PriceDisplay() { + const price = useContent(); + return ${price.toFixed(2)}; +} + + + {/* Displays: $99.99 */} +; +``` + +### With Override Parameter + +```tsx +function UserGreeting({ name }: { name?: string }) { + const userName = useContent(name); + return

Hello, {userName}!

; +} + +// Standalone with prop + + +// With context + + + + +// Context with override (override wins) + + {/* Uses "Dave", not "Charlie" */} + +``` + +## Related + +- [Micro-Context Pattern](./MICRO-CONTEXT.md) - Pattern overview +- [FieldContent](./Contents.md#fieldcontent) - Access object fields +- [ArrayContent](./ArrayContent.md) - Map arrays +- [DefaultContent](./Contents.md#defaultcontent) - Render primitive values diff --git a/packages/ctablex-core/docs/Contents.md b/packages/ctablex-core/docs/Contents.md new file mode 100644 index 0000000..888a1f4 --- /dev/null +++ b/packages/ctablex-core/docs/Contents.md @@ -0,0 +1,538 @@ +# Content Components + +Utility components for accessing fields, transforming values, and handling null/undefined. + +## TL;DR + +- Use `` to render primitive values +- Use `` for flexible value transformation (paths, functions) +- Use `` to access object properties +- Use `` and `` for null/undefined handling + +## DefaultContent + +Renders primitive values (string, number, null, undefined) directly. + +### Props + +None. + +### Behavior + +- Retrieves the content value from context +- Renders it directly (React's default behavior for primitives) +- Used as the default `children` for most content components +- **Only works with primitives** - objects and arrays of objects will cause React errors +- Arrays of primitives work fine + +### Example + +```tsx +import { DefaultContent } from '@ctablex/core'; + +// String + + {/* Renders: Hello */} + + +// Number + + {/* Renders: 42 */} + + +// Boolean (renders nothing - React's default) + + {/* Renders: (nothing) */} + + +// Null + + {/* Renders: (nothing) */} + + +// ✗ Common mistake - objects cause React errors + + {/* ✗ Error: Objects are not valid as a React child */} + + +// ✓ Use FieldContent or ObjectContent for objects + + + + + +``` + +### Default Usage + +Most content components use `` as their default children: + +```tsx +// These are equivalent: + + + + + +// Also equivalent: + + + + +``` + +### Common Pitfall + +Many content components use `` as default children. This causes errors when the content value is an object or array of objects: + +```tsx +// ✗ Error - ArrayContent provides array elements (objects) to DefaultContent + + {/* Missing children - defaults to */} + +// ✗ Error: Objects are not valid as a React child + +// ✓ Provide explicit children for object/array content + + + + + + + + +// ✗ Error - FieldContent provides object value to DefaultContent + + {/* Missing children - defaults to */} + + +// ✗ Error: Objects are not valid as a React child + +// ✓ Nest FieldContent or use ObjectContent + + + + + + + +``` + +**Remember:** `` only works with primitives (string, number, boolean, null, undefined) or arrays of primitives. For objects and arrays, you must provide explicit children. + +**Note on booleans:** While `` accepts boolean values without error, React renders nothing for `true` or `false` by default. To display boolean values, provide a custom component: + +```tsx +// React renders nothing for booleans + + {/* Renders: (nothing) */} + + +// ✓ Use a custom component to display boolean values + + {/* Renders: Yes */} + +// Renders: Yes +``` + +## ContentValue + +Transforms the content value using an accessor (path, function, undefined, or null), then provides the result to children. + +> **Note:** This component was previously named `AccessorContent`. The old name is still available as an alias for backward compatibility. + +### Props + +```tsx +interface ContentValueProps { + accessor: Accessor; + children?: ReactNode; + value?: V; +} +``` + +- **`accessor`** - Accessor to apply to the content value + - Path string: `"user.address.city"` + - Function: `(value) => value.computed` + - `undefined`: Returns value unchanged + - `null`: Returns null +- **`children`** - Content to render with transformed value (defaults to ``) +- **`value`** - Input value to transform (optional, uses context value if omitted) + +### Behavior + +- Retrieves the content value from context (or uses `value` prop) +- Applies the accessor to transform the value +- Provides the transformed value to children via a new `ContentProvider` + +### Example + +```tsx +import { ContentValue, DefaultContent } from '@ctablex/core'; + +type User = { + profile: { + name: string; + age: number; + }; + isActive: boolean; +}; + +const user: User = { + profile: { name: 'Alice', age: 30 }, + isActive: true, +}; + +// Path accessor + + + {/* Renders: Alice */} + + + +// Function accessor + + u.profile.age > 18}> + {/* Renders: Yes */} + + + +// Undefined accessor (returns unchanged) + + + + {/* Renders: true */} + + + + +// Null accessor + + + + + + + +// Renders: No value +``` + +### Type Safety + +The `accessor` prop is validated based on the generic type: + +```tsx +type Product = { + name: string; + price: number; + metadata: { + weight: number; + }; +}; + +// ✓ Type-safe paths with autocomplete + accessor="metadata.weight"> + + + +// ✗ Compile error - invalid path + accessor="metadata.height"> + + + +// ✓ Type-safe function + accessor={(p) => p.price * 1.1}> + + +``` + +**Note:** The generic type `` itself is not validated by TypeScript (you could pass the wrong type), but **given** that type, the `accessor` prop is fully validated with autocomplete. + +## FieldContent + +Accesses a single field of an object and provides its value to children. Simplified version of `ContentValue` for object properties. + +### Props + +```tsx +interface FieldContentProps { + field: keyof V; + children?: ReactNode; +} +``` + +- **`field`** - The object property to access +- **`children`** - Content to render with field value (defaults to ``) + +### Behavior + +- Retrieves the content value from context +- Accesses the specified field +- Provides the field value to children via a new `ContentProvider` + +### Example + +```tsx +import { FieldContent, DefaultContent } from '@ctablex/core'; + +type User = { + name: string; + email: string; + age: number; + address: { + city: string; + }; +}; + +const user: User = { + name: 'Bob', + email: 'bob@example.com', + age: 25, + address: { + city: 'New York', + }, +}; + +// Access single field + + + {/* Renders: Bob */} + + + +// Nested field access (field within field) + + + + {/* Renders: New York */} + + + + +// Multiple fields + + Name: + Email: + +// Renders: Name: Bob, Email: bob@example.com + + +// ✗ Common mistake - trying to nest fields from parent context + + + Name: , + Email: + + +// ✗ Error! "email" is a string, not an object with a "name" field +``` + +### Type Safety + +The `field` prop is validated based on the generic type: + +```tsx +type Product = { + name: string; + price: number; +}; + +// ✓ Valid field with autocomplete + field="name"> + + + +// ✗ Compile error - field doesn't exist + field="description"> + + +``` + +**Note:** You must add the generic type `` to get type checking and autocomplete for the `field` prop. Without it, no validation occurs: + +```tsx +// ✗ No type checking - accepts any string + + + +``` + +### Comparison with ContentValue + +`FieldContent` is simpler but less flexible: + +```tsx +// FieldContent - simple, direct property access + + + + +// ContentValue - more flexible, supports nested paths + + + +``` + +## NullableContent + +Conditionally renders content based on whether the value is null or undefined. + +### Props + +```tsx +interface NullableContentProps { + children?: ReactNode; + nullContent?: ReactNode; +} +``` + +- **`children`** - Content to render when value is not null/undefined (defaults to ``) +- **`nullContent`** - Content to render when value is null/undefined (defaults to `null`) + +### Behavior + +- Retrieves the content value from context +- If value is `null` or `undefined`, renders `nullContent` +- Otherwise, renders `children` + +### Example + +```tsx +import { NullableContent, DefaultContent } from '@ctablex/core'; + +type User = { + name: string; + email?: string; +}; + +const user1: User = { name: 'Alice', email: 'alice@example.com' }; +const user2: User = { name: 'Bob', email: undefined }; + +// With value + + + + + + + +// Renders: alice@example.com + +// Without value + + + + + + + +// Renders: No email + +// Default behavior (renders nothing for null) + + + + + +// Renders: (nothing) +``` + +### Use Cases + +- Display fallback text for missing optional fields +- Hide content when data is unavailable + +## NullContent + +Conditionally renders children only when the content value is `null` or `undefined`. + +`NullContent` is useful for rendering complex fallback content when the value is missing. It is the opposite of `NullableContent`, which renders content when the value exists. For simpler cases, you can use the `nullContent` prop of `NullableContent`. + +### Props + +```tsx +interface NullContentProps { + children?: ReactNode; +} +``` + +- **`children`** - Content to render when value is null or undefined + +### Behavior + +- Retrieves the content value from context +- Renders children if value is `null` or `undefined` +- Returns `null` otherwise (renders nothing) + +### Example + +```tsx +import { NullContent, ContentProvider } from '@ctablex/core'; + +// Renders children when null + + +
No data available
+
+
+// Renders:
No data available
+ +// Renders children when undefined + + +
No data available
+
+
+// Renders:
No data available
+ +// Renders nothing when value exists + + +
No data available
+
+
+// Renders: (nothing) + +// Renders nothing for empty string + + +
No data available
+
+
+// Renders: (nothing) - empty string is not null/undefined +``` + +### Use Cases + +- Display fallback UI when data is missing +- Render complex content when value is null (for simpler cases, use the `nullContent` prop of `NullableContent`) + +```tsx +type User = { + name: string; + email?: string; +}; + +const user: User = { name: 'Alice' }; + + + + + Email not provided + + + + + +; + +// Renders: Email not provided +``` + +## Related + +- [ArrayContent](./ArrayContent.md) - Iterate over arrays +- [ObjectContent](./ObjectContent.md) - Iterate over objects +- [ContentContext](./ContentContext.md) - Content value context +- [Accessors](./Accessors.md) - Type-safe value extraction +- [Micro-Context Pattern](./MICRO-CONTEXT.md) - Pattern overview diff --git a/packages/ctablex-core/docs/MICRO-CONTEXT.md b/packages/ctablex-core/docs/MICRO-CONTEXT.md new file mode 100644 index 0000000..b7ffc7c --- /dev/null +++ b/packages/ctablex-core/docs/MICRO-CONTEXT.md @@ -0,0 +1,326 @@ +# Micro-Context Pattern + +## What is Micro-Context? + +**Micro-context** is a pattern for passing data through localized React Context instead of props. Unlike traditional "macro-context" patterns (like theme providers or auth state that span entire applications), micro-context creates small, scoped context providers within component subtrees for fine-grained data flow. + +This enables **declarative data transformation** with minimal manual prop passing—components describe what data they need, not how to pass it through every layer. + +## The Problem It Solves + +In traditional React patterns, data flows through props manually: + +```tsx + + {data.map((item) => ( + + + + + + ))} +
+``` + +This leads to: + +- **Prop drilling** - Every intermediate component must accept and pass props +- **Tight coupling** - Child components are explicitly bound to parent props +- **Limited composition** - Hard to create reusable renderers that work in different contexts + +## The Micro-Context Solution + +Instead of passing data as props, wrap it in a context provider, no data passed via props or manual iteration: + +```tsx + + + {/* Iterates array, provides each item via context */} + + + {/* Extracts "price" field, provides it via context */} + + + {/* Reads value from context */} + + + + + +
+
+``` + +Notice that no props are passed through `Table`, `Row`, or `Cell`. Each component declares what data it needs, and micro-context handles the flow automatically. This enables powerful patterns for building flexible, composable components. + +Child components access data through hooks: + +```tsx +function NumberFormatter() { + const value = useContent(); // gets value from nearest context + return <>{formatNumber(value)}; +} +``` + +## Key Characteristics + +### 1. **Reusable Renderers** + +Components that consume context work anywhere without knowing the data source: + +```tsx +function BooleanContent({ yes, no }) { + const value = useContent(); + return <>{value ? yes : no}; +} + +// Works in any context that provides a boolean + + +; +``` + +### 2. **Immutable Children & Performance** + +Since data flows through context instead of props, React elements often have no changing props, making them **immutable**. This enables powerful optimizations: + +```tsx +// Children can be memoized +const defaultChildren = ; + +function FieldContent({ field }) { + const content = useContent(); + return ( + + {defaultChildren} {/* Same reference every render */} + + ); +} + +// Or memoized within the component +function ProductTable() { + return useMemo( + () => ( + + + + + + ), + [], + ); +} + +// Or even moved outside the component +const content = ( + + + + + +); +// component +function ProductTable() { + return content; +} +``` + +Immutable children help React's reconciliation algorithm skip unnecessary re-renders and comparisons. + +### 3. **Default Children** + +Components can provide sensible defaults, reducing boilerplate: + +```tsx +// With default children + + +// Equivalent to + + + + +// Implementation +const defaultChildren = ; +function ContentValue({ accessor, children = defaultChildren }) { + const content = useContent(); + return ( + + {children} + + ); +} +``` + +This makes simple cases concise while keeping customization available. + +### 4. **Open for Customization** + +Default children create a pattern where components work out-of-the-box but remain fully customizable: + +```tsx +// Default usage - simple and clean + + +// Custom rendering - override when needed + + + + +// Complex composition - mix defaults and custom + + + + : + + + +``` + +This pattern balances **ease of use** (good defaults) with **flexibility** (full customization). + +## Core Concepts + +### ContentProvider & useContent + +The foundation of micro-context: + +```tsx +// Provide data via context + + +; + +// Consume data from context +function MyComponent() { + const data = useContent(); + return
{data}
; +} +``` + +### Content Components + +Components that transform data and provide it via context: + +- **ContentValue** - Extract data using path or function accessors +- **FieldContent** - Access object fields +- **ArrayContent** - Map arrays, providing each item via context +- **ObjectContent** - Iterate object keys, providing values via context +- **NullableContent** - Handle null/undefined values +- **DefaultContent** - Render primitive values + +### Accessors + +Extract values from data structures: + +```tsx +// Path accessor - string-based + + + + +// Function accessor + user.fullName}> + + +``` + +### Additional Contexts + +Beyond value context, micro-contexts can provide metadata: + +- **IndexContext** - Current index in array/object iteration +- **KeyContext** - Current key in object iteration + +```tsx + + {/* renders 0, 1, 2... */} + + +``` + +## Trade-offs and Limitations + +### Weak Type Safety + +The biggest drawback of micro-context is the **lack of strong type safety**. While TypeScript provides autocomplete and validation for props, it cannot enforce correctness across context boundaries. + +#### How It Works + +TypeScript type safety in ctablex has three key characteristics: + +1. **Generic types on components are NOT validated** - You can pass wrong types without errors +2. **Props ARE validated based on the generic** - Given a type, props get autocomplete +3. **Nested components are NOT automatically typed** - Must add explicit generics to each + +This means **generic types must be explicitly passed** to both content components and the `useContent` hook: + +```tsx +type User = { name: string; address: { city: string } }; + +// ✓ Type-safe with explicit generic - field is validated and autocompleted + field="name"> + +
+ +// ✗ No type checking without generic - any field accepted + + + + +// ✓ Nested components need their own generics + getKey="id"> + field="name" /> {/* Must specify User again */} + + +// useContent also requires manual type annotation +function MyComponent() { + const user = useContent(); // ✗ Type is just a hint - not validated + return
{user.name}
; +} +``` + +#### Why This Happens + +- **JSX elements types are erased at compile time** to `ReactElement` so no type information flows to children +- **JSX elements cannot be validated at compile time** to infer or verify generics +- **TypeScript cannot verify that the generic type matches the actual context value** +- **`useContent()` type is purely manual** - TypeScript cannot check it matches context +- **Type safety depends entirely on developer discipline** + +#### Implications + +- **Runtime errors** - Typos in field names won't be caught at compile time +- **Refactoring risks** - Renaming fields may not update all references +- **Developer burden** - Must manually ensure type correctness throughout the component tree +- **Silent failures** - Wrong types compile successfully but fail at runtime +- **No autocomplete without generics** - IDEs can't suggest valid field names + +## When to Use Micro-Context + +Micro-context excels when: + +- Building **repetitive structures** (tables, lists) +- Creating **reusable renderers** that work with different data +- Providing **high customizability** - Components using micro-context enable flexible composition and customization + +## Micro vs Macro Context + +| Aspect | Macro-Context | Micro-Context | +| -------- | ------------------------------- | ----------------------------- | +| Scope | Application-wide | Component subtree | +| Lifetime | Entire app lifecycle | Component render | +| Updates | Infrequent, triggers re-renders | Frequent, localized | +| Purpose | Global state (theme, auth) | Data transformation & flow | +| Nesting | Typically 1-2 levels | Multiple nested levels | +| Examples | ThemeProvider, AuthProvider | ContentProvider, ArrayContent | + +## Summary + +Micro-context is a pattern for **localized, granular data flow** using React Context. By creating small, scoped providers within component trees, it enables: + +- Data transformation without prop drilling +- Reusable components that consume from context +- Flexible composition of transformers and renderers + +This approach unlocks powerful patterns for building flexible, composable UIs while maintaining clean component boundaries. However, this flexibility comes at the cost of weaker compile-time type safety compared to traditional props. diff --git a/packages/ctablex-core/docs/ObjectContent.md b/packages/ctablex-core/docs/ObjectContent.md new file mode 100644 index 0000000..148d20a --- /dev/null +++ b/packages/ctablex-core/docs/ObjectContent.md @@ -0,0 +1,241 @@ +# Object Content Components + +Components for iterating over object properties and accessing object keys. + +## TL;DR + +- Use `` to iterate over object properties +- Use `` to display the current property key +- Use `KeyContext`, `useKey` to access the current property key + +## ObjectContent + +Iterates over object properties, rendering children for each key-value pair. Provides the property value, key, and index via context. + +### Props + +```tsx +interface ObjectContentProps { + getKey?: ObjectGetKey; + children: ReactNode; + join?: ReactNode; + value?: V; +} + +type ObjectGetKey = ( + value: V[K], + key: K, + index: number, +) => string | number; +``` + +- **`getKey`** - Generates a unique React key for each property (optional, defaults to property key) +- **`children`** - Content to render for each property (required) +- **`join`** - Content to render between properties (e.g., commas, separators) +- **`value`** - Object to iterate (optional, uses context value if omitted) + +### Behavior + +- Iterates over `Object.keys()` of the object from content context +- Provides each property value via `ContentProvider` +- Provides the property key via `KeyContext` +- Provides the iteration index via `IndexContext` +- Renders `join` content between properties (not before the first property) +- Uses `getKey` to generate React keys for list items (defaults to `key.toString()`) + +### Example + +```tsx +import { ObjectContent, KeyContent, IndexContent } from '@ctablex/core'; + +type Product = { + name: string; + price: number; + stock: number; +}; + +const product: Product = { + name: 'Widget', + price: 99.99, + stock: 50, +}; + +// Basic iteration + + + : + + +// Renders: name: Widgetprice: 99.99stock: 50 + +// With separator + + + : + + +// Renders: name: Widget, price: 99.99, stock: 50 + +// With index + + }> + . : + + +// Renders: +// 1. name: Widget +// 2. price: 99.99 +// 3. stock: 50 + +// Custom key function + + `prop-${index}`}> + : + + +``` + +### Type Safety + +The generic type `V` on `ObjectContent` provides type safety for the `getKey` prop: + +```tsx +type User = { + name: string; + age: number; +}; + +// ✓ Type-safe getKey function + + getKey={(value, key, index) => { + // value: string | number (union of User property types) + // key: "name" | "age" (User keys) + return `${String(key)}-${index}`; + }} +> + : +; +``` + +However, **nested components do NOT receive type information** from `ObjectContent`: + +```tsx +// Generic on ObjectContent alone doesn't provide type checking for nested components +> + {/* string | number | symbol - always this type */} + {/* any - no type information from ObjectContent */} + +``` + +**Summary:** + +- `ObjectContent` generic provides type safety for `getKey` prop +- Nested components do NOT inherit the type - each needs its own explicit generic if type safety is needed + +## KeyContent + +Displays the current object property key from `KeyContext`. + +### Props + +None. + +### Behavior + +- Retrieves the current property key from `KeyContext` +- Renders the key as a string + +### Example + +```tsx +const user = { + firstName: 'John', + lastName: 'Doe', + age: 30, +}; + + + }> + : + +; +// Renders: +// firstName: John +// lastName: Doe +// age: 30 +``` + +### Error Handling + +Throws an error if used outside `KeyContext`: + +```tsx +// ✗ Error: useKey must be used within a KeyContext + +``` + +## KeyContext + +React Context providing the current object property key. + +```tsx +const KeyContext: React.Context; +function useKey(): string | number | symbol; +``` + +### Behavior + +- Automatically provided by `ObjectContent` +- Contains the current property key +- `useKey()` throws if context is undefined + +### Example + +```tsx +import { useKey } from '@ctablex/core'; + +function CustomKeyDisplay() { + const key = useKey(); + return {String(key).toUpperCase()}; +} + +const data = { name: 'Alice', age: 25 }; + + + + : + +; +// Renders: NAME: Alice, AGE: 25 +``` + +## IndexContext + +The iteration index is also provided via `IndexContext` (see [ArrayContent](./ArrayContent.md#indexcontext)). + +### Example + +```tsx +const settings = { + theme: 'dark', + language: 'en', + notifications: true, +}; + + + }> + : = + +; +// Renders: +// 0: theme = dark +// 1: language = en +// 2: notifications = true +``` + +## Related + +- [ArrayContent](./ArrayContent.md) - Iterate over arrays +- [ContentContext](./ContentContext.md) - Content value context +- [FieldContent](./Contents.md#fieldcontent) - Access single object field +- [Micro-Context Pattern](./MICRO-CONTEXT.md) - Pattern overview diff --git a/packages/ctablex-core/docs/README.md b/packages/ctablex-core/docs/README.md new file mode 100644 index 0000000..ede4025 --- /dev/null +++ b/packages/ctablex-core/docs/README.md @@ -0,0 +1,235 @@ +# Documentation Guide + +Welcome to the ctablex-core documentation! This guide will help you navigate and understand the documentation structure. + +## Quick Start + +If you're new to ctablex, start here: + +1. **[Micro-Context Pattern](./MICRO-CONTEXT.md)** - Understand the core concept behind ctablex +2. **[ContentContext](./ContentContext.md)** - Learn about the foundation: ContentProvider and useContent +3. **[Contents](./Contents.md)** - Explore basic content transformation components + +## Documentation Structure + +### Core Concepts + +#### [Micro-Context Pattern](./MICRO-CONTEXT.md) + +The fundamental pattern that ctablex is built on. Read this first to understand: + +- What micro-context means (localized context vs app-wide context) +- Key characteristics: immutable children, default children, context nesting +- Benefits and trade-offs (especially type safety limitations) +- When to use this pattern + +### Foundation + +#### [ContentContext](./ContentContext.md) + +The building blocks of the micro-context system: + +- **ContentProvider** - Wraps values and provides them to children +- **useContent** - Hook to retrieve values from context +- **ContentContext** - Internal React Context (rarely used directly) + +### Content Components + +Components for transforming and accessing data: + +#### [Contents](./Contents.md) + +Utility components for common operations: + +- **ContentValue** - Transform values using paths or functions +- **FieldContent** - Access object properties +- **NullableContent**, **NullContent** - Handle null/undefined values +- **DefaultContent** - Render primitive values + +#### [ArrayContent](./ArrayContent.md) + +Components for array iteration: + +- **ArrayContent** - Iterate over arrays with index support +- **IndexContent** - Display current iteration index +- **IndexContext** - Context providing array index +- **EmptyContent**, **NonEmptyContent** - Conditional rendering based on array emptiness + +#### [ObjectContent](./ObjectContent.md) + +Components for object iteration: + +- **ObjectContent** - Iterate over object properties +- **KeyContent** - Display current property key +- **KeyContext** - Context providing property key + +### Type-Safe Data Access + +#### [Accessors](./Accessors.md) + +Strong type safety for value extraction: + +- **Path Accessors** - `accessByPath`, `accessByPathTo` with autocomplete +- **Function Accessors** - `accessByFn` with type inference +- **Unified Accessors** - `access`, `accessTo` accepting any accessor type +- **Type Definitions** - `PathAccessor`, `Accessor`, and related types + +## Common Patterns + +### Basic Value Display + +```tsx + + + + + +``` + +Start with: [Contents](./Contents.md#fieldcontent) + +### Array Iteration + +```tsx + + + + + + + +``` + +Start with: [ArrayContent](./ArrayContent.md) + +### Object Iteration + +```tsx + + + : + + +``` + +Start with: [ObjectContent](./ObjectContent.md) + +### Nested Path Access + +```tsx + + + + + +``` + +Start with: [Contents](./Contents.md#contentvalue) and [Accessors](./Accessors.md) + +### Conditional Rendering + +```tsx + + + + + + + +``` + +Start with: [Contents](./Contents.md#nullablecontent) + +## Type Safety Limitations + +⚠️ Micro-context provides weak type safety. Generic types must be manually specified and cannot be validated across context boundaries. See [MICRO-CONTEXT.md - Weak Type Safety](./MICRO-CONTEXT.md#weak-type-safety) for details. + +## Common Pitfalls + +### useContent in JSX Children + +**Problem:** Calling `useContent()` directly in JSX children runs in the **parent** component's context, not the nested one. + +```tsx +// ✗ Wrong - useContent() runs in parent, gets wrong value + + {useContent() ? 'Yes' : 'No'} +; + +// ✓ Correct - create a separate component +function BooleanDisplay() { + const value = useContent(); + return <>{value ? 'Yes' : 'No'}; +} + + + +; +``` + +**Why?** React evaluates JSX children before passing them to components. The `useContent()` call happens in the parent's render, not inside `FieldContent`. + +Read more: [ContentContext - useContent](./ContentContext.md#usecontent) + +### DefaultContent with Objects + +**Problem:** DefaultContent only works with primitives (string, number, boolean, null, undefined). + +```tsx +// ✗ Error - objects cause React errors + {/* Defaults to but elements are objects */} + +// ✓ Provide explicit children + + + + + +``` + +Read more: [Contents - DefaultContent](./Contents.md#defaultcontent) + +### Missing Generic Types + +**Problem:** Forgetting to add generic types means no autocomplete or validation. + +```tsx +// ✗ No type checking + + +// ✓ Type checking with autocomplete + field="name" /> +``` + +Read more: Each component's Type Safety section + +### Path Depth Limitation + +**Problem:** Path accessors only support up to 5 levels of nesting. + +```tsx +// ✗ Too deep + + +// ✓ Use function accessor instead + obj.a.b.c.d.e.f} /> +``` + +Read more: [Accessors - Limitations](./Accessors.md#limitations) + +## Document Index + +- **[MICRO-CONTEXT.md](./MICRO-CONTEXT.md)** - Core pattern explanation +- **[ContentContext.md](./ContentContext.md)** - ContentProvider, useContent, ContentContext +- **[Contents.md](./Contents.md)** - ContentValue, FieldContent, NullableContent, DefaultContent +- **[ArrayContent.md](./ArrayContent.md)** - ArrayContent, IndexContent, IndexContext +- **[ObjectContent.md](./ObjectContent.md)** - ObjectContent, KeyContent, KeyContext +- **[Accessors.md](./Accessors.md)** - All accessor functions and types + +## Need Help? + +1. **Getting Started** → Read [MICRO-CONTEXT.md](./MICRO-CONTEXT.md) first +2. **Basic Usage** → Check [ContentContext.md](./ContentContext.md) and [Contents.md](./Contents.md) +3. **Iteration** → See [ArrayContent.md](./ArrayContent.md) or [ObjectContent.md](./ObjectContent.md) +4. **Advanced Types** → Explore [Accessors.md](./Accessors.md) +5. **Type Safety Issues** → Review the Type Safety sections in each document diff --git a/packages/ctablex-core/index.d.ts b/packages/ctablex-core/index.d.ts index 33b78f8..ad4593e 100644 --- a/packages/ctablex-core/index.d.ts +++ b/packages/ctablex-core/index.d.ts @@ -2,39 +2,78 @@ import { Context } from 'react'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { ReactNode } from 'react'; +/** + * Accesses a value using a path string, function, undefined, or null. + * - undefined returns the input unchanged + * - null returns null + * - string uses accessByPath + * - function calls the function with the input + * @param t - The object to access + * @param a - The accessor (path, function, undefined, or null) + * @returns The accessed value + */ export declare function access>( t: T, a: A, ): AccessorValue; +/** + * Accesses a value using a custom extraction function. + * @param obj - The object to access + * @param fn - Function that extracts the value + * @returns The result of calling fn with obj + */ export declare function accessByFn>( obj: T, fn: F, ): FnAccessorValue; +/** + * Accesses a nested property using a dot-separated string path. + * Provides full type safety with autocomplete and compile-time error detection. + * @param t - The object to access + * @param path - Dot-separated path like "user.address.city" + * @returns The value at the specified path + */ export declare function accessByPath>( t: T, path: K, ): PathAccessorValue; +/** + * Accesses a nested property using a path constrained to return a specific type. + * Like accessByPath but only accepts paths that return values of type R. + * @param t - The object to access + * @param path - Dot-separated path that returns type R + * @returns The value at the specified path, typed as R + */ export declare function accessByPathTo< R, T, K extends PathAccessorTo = PathAccessorTo, >(t: T, path: K): R & PathAccessorValue; +/** + * Union type accepting path strings, functions, undefined, or null. + */ export declare type Accessor = | undefined | null | PathAccessor | FnAccessor; +/** + * Union type accepting accessors constrained to return a specific type. + */ export declare type AccessorTo = | undefined | null | PathAccessorTo | FnAccessor; +/** + * The type of the value returned by an accessor. + */ export declare type AccessorValue< T, A extends Accessor, @@ -48,6 +87,13 @@ export declare type AccessorValue< ? FnAccessorValue : never; +/** + * Accesses a value using an accessor constrained to return a specific type. + * Like access but only accepts accessors that return values of type R. + * @param t - The object to access + * @param a - The accessor constrained to return type R + * @returns The accessed value, typed as R + */ export declare function accessTo< R, T, @@ -63,17 +109,30 @@ declare type AllowedIndexes< ? AllowedIndexes : Keys; +/** + * Iterates over an array, rendering children for each element. + * Provides both the array element via ContentProvider and its index via IndexContext. + * + * Default children: + */ export declare function ArrayContent( props: ArrayContentProps, ): JSX_2.Element; export declare interface ArrayContentProps { + /** Extracts unique key from each element (path or function). Defaults to index. */ getKey?: PathAccessorTo | ArrayGetKey; + /** Content to render for each element. Defaults to . */ children?: ReactNode; + /** Content to render between elements (e.g., commas, separators). */ join?: ReactNode; + /** Array to iterate. If omitted, uses context value. */ value?: ReadonlyArray; } +/** + * Function type for extracting a unique key from array elements. + */ declare type ArrayGetKey = (value: V, index: number) => string | number; declare type ComputeRange< @@ -83,23 +142,49 @@ declare type ComputeRange< ? Result : ComputeRange; +/** + * The underlying React Context for the micro-context pattern. + * @internal This is an internal API and may change in future versions. + * Use `ContentProvider` and `useContent` instead. + */ export declare const ContentContext: Context< ContentContextType | undefined >; +/** + * The type of the content context value. + * @internal This is an internal API and may change in future versions. + */ export declare type ContentContextType = { value: V; }; +/** + * Provides a content context that can be retrieved with useContent. + * Providers can be nested to create scoped contexts. + */ export declare function ContentProvider( props: ContentProviderProps, ): JSX_2.Element; +/** + * Props for ContentProvider. + */ export declare interface ContentProviderProps { + /** The value to provide via context. */ value: V; children?: ReactNode; } +/** + * Transforms the content value using an accessor, then provides the result to children. + * - Path string: Accesses nested properties like "user.address.city" + * - Function: Calls the function with the content value + * - undefined: Returns the content value unchanged + * - null: Returns null + * + * Default children: + */ declare function ContentValue(props: ContentValueProps): JSX_2.Element; export { ContentValue as AccessorContent }; export { ContentValue }; @@ -112,17 +197,39 @@ declare interface ContentValueProps { export { ContentValueProps as AccessorContentProps }; export { ContentValueProps }; +/** + * Renders primitive values (string, number, null, undefined) directly from context. + * Used as the default children for most content components. + * Only works with primitives - objects and arrays of objects will cause React errors. + */ export declare function DefaultContent(): JSX_2.Element; +/** + * Renders its children only when the content is null, undefined, or empty. + * @remarks + * Uses {@link useContent} to access the current content from the context. + * By default, only arrays with length 0 are considered empty. + */ export declare function EmptyContent( props: EmptyContentProps, ): JSX_2.Element | null; +/** + * Props for the {@link EmptyContent} component. + */ export declare interface EmptyContentProps { + /** Content to render when the content is empty. */ children?: ReactNode; + /** Custom function to determine if content is empty. By default, only arrays with length 0 are considered empty. */ isEmpty?: (content: C) => boolean; } +/** + * Accesses a single field of an object and provides its value to children. + * Simplified version of AccessorContent for object properties. + * + * Default children: + */ export declare function FieldContent( props: FieldContentProps, ): JSX_2.Element; @@ -132,8 +239,14 @@ export declare interface FieldContentProps { children?: ReactNode; } +/** + * A function that extracts a value from an object. + */ export declare type FnAccessor = (t: T) => R; +/** + * The return type of a function accessor. + */ export declare type FnAccessorValue> = F extends { (t: T, ...args: any[]): infer R; (t: T, ...args: any[]): any; @@ -178,12 +291,20 @@ export declare type FnAccessorValue> = F extends { declare type Index40 = ComputeRange<40>[number]; +/** + * Displays the current array or object iteration index from IndexContext. + * Optionally adds a start offset to the index. + */ export declare function IndexContent(props: IndexContentProps): JSX_2.Element; export declare interface IndexContentProps { start?: number; } +/** + * Context providing the current array or object iteration index. + * Used internally by ArrayContent and ObjectContent. + */ export declare const IndexContext: Context; declare type IsTuple = T extends readonly any[] & { @@ -194,19 +315,43 @@ declare type IsTuple = T extends readonly any[] & { : never : never; +/** + * Displays the current object property key from KeyContext. + */ export declare function KeyContent(): JSX_2.Element; +/** + * Context providing the current object property key during iteration. + * Used internally by ObjectContent. + */ export declare const KeyContext: Context; +/** + * Renders its children only when the content is not null, not undefined, and not empty. + * @remarks + * Uses {@link useContent} to access the current content from the context. + * By default, only arrays with length 0 are considered empty. + */ export declare function NonEmptyContent( props: NonEmptyContentProps, ): JSX_2.Element | null; +/** + * Props for the {@link NonEmptyContent} component. + */ export declare interface NonEmptyContentProps { + /** Content to render when the content is not empty. */ children?: ReactNode; + /** Custom function to determine if content is empty. By default, only arrays with length 0 are considered empty. */ isEmpty?: (content: C) => boolean; } +/** + * Conditionally renders content based on whether the value is null or undefined. + * Renders nullContent when value is null/undefined, otherwise renders children. + * + * Default children: + */ export declare function NullableContent( props: NullableContentProps, ): JSX_2.Element; @@ -216,31 +361,56 @@ export declare interface NullableContentProps { nullContent?: ReactNode; } +/** + * Renders its children only when the content is null or undefined. + * @remarks + * Uses {@link useContent} to access the current content from the context. + */ export declare function NullContent( props: NullContentProps, ): JSX_2.Element | null; +/** + * Props for the {@link NullContent} component. + */ export declare interface NullContentProps { + /** Content to render when the content is null or undefined. */ children?: ReactNode; } +/** + * Iterates over object properties, rendering children for each key-value pair. + * Provides the property value via ContentProvider, key via KeyContext, and index via IndexContext. + */ export declare function ObjectContent( props: ObjectContentProps, ): JSX_2.Element; export declare interface ObjectContentProps { + /** Generates unique React key for each property. Defaults to property key. */ getKey?: ObjectGetKey; + /** Content to render for each property. */ children: ReactNode; + /** Content to render between properties (e.g., commas, separators). */ join?: ReactNode; + /** Object to iterate. If omitted, uses context value. */ value?: V; } +/** + * Function type for generating React keys from object properties. + */ declare type ObjectGetKey = ( value: V[K], key: K, index: number, ) => string | number; +/** + * String literal type representing valid dot-separated paths through an object. + * Supports nested properties up to 5 levels deep. + * @example "user.address.city" + */ export declare type PathAccessor< T, TDepth extends any[] = [], @@ -259,11 +429,18 @@ export declare type PathAccessor< : never) & string; +/** + * String literal type representing paths through an object that return a specific type. + * Filters PathAccessor to only include paths where the value extends R. + */ export declare type PathAccessorTo = { [K in PathAccessor]: PathAccessorValue extends R ? K : never; }[PathAccessor] & string; +/** + * The type of the value at a given path in an object. + */ export declare type PathAccessorValue = 0 extends 1 & T ? any : T extends null | undefined @@ -284,10 +461,26 @@ declare type PathPrefix< ? `${TPrefix}.${PathAccessor & string}` : never; +/** + * Retrieves the current value from the nearest ContentProvider. + * @param value - Optional override value. If provided, returns this value instead of context. + * @returns The content value from context or the override value. + * @throws Error if called outside a ContentProvider and no override value is provided. + */ export declare function useContent(value?: V): V; +/** + * Retrieves the current iteration index from ArrayContent or ObjectContent. + * @returns The zero-based iteration index. + * @throws Error if called outside an ArrayContent or ObjectContent. + */ export declare function useIndex(): number; +/** + * Retrieves the current object property key from ObjectContent. + * @returns The property key (string, number, or symbol). + * @throws Error if called outside an ObjectContent. + */ export declare function useKey(): string | number | symbol; export {}; diff --git a/packages/ctablex-core/package.json b/packages/ctablex-core/package.json index 0e88372..aa7dabe 100644 --- a/packages/ctablex-core/package.json +++ b/packages/ctablex-core/package.json @@ -1,11 +1,31 @@ { "name": "@ctablex/core", "version": "0.6.5", + "description": "Core building blocks for composable, context-based React components using the micro-context pattern", + "keywords": [ + "react", + "context", + "composition", + "micro-context", + "declarative", + "typescript" + ], + "homepage": "https://github.com/ctablex/core#readme", + "repository": { + "type": "git", + "url": "https://github.com/ctablex/core.git", + "directory": "packages/ctablex-core" + }, + "bugs": { + "url": "https://github.com/ctablex/core/issues" + }, "files": [ "src", "dist", "index.d.ts", - "tsdoc-metadata.json" + "tsdoc-metadata.json", + "README.md", + "docs" ], "license": "MIT", "type": "module", diff --git a/packages/ctablex-core/src/accessor/accessor.ts b/packages/ctablex-core/src/accessor/accessor.ts index 6971499..1c5e4f9 100644 --- a/packages/ctablex-core/src/accessor/accessor.ts +++ b/packages/ctablex-core/src/accessor/accessor.ts @@ -6,12 +6,21 @@ import { PathAccessorValue, } from './path-accessor'; +/** + * Union type accepting path strings, functions, undefined, or null. + */ export type Accessor = undefined | null | PathAccessor | FnAccessor; +/** + * Union type accepting accessors constrained to return a specific type. + */ export type AccessorTo = | undefined | null | PathAccessorTo | FnAccessor; +/** + * The type of the value returned by an accessor. + */ export type AccessorValue> = A extends undefined ? T : A extends null @@ -22,6 +31,16 @@ export type AccessorValue> = A extends undefined ? FnAccessorValue : never; +/** + * Accesses a value using a path string, function, undefined, or null. + * - undefined returns the input unchanged + * - null returns null + * - string uses accessByPath + * - function calls the function with the input + * @param t - The object to access + * @param a - The accessor (path, function, undefined, or null) + * @returns The accessed value + */ export function access>( t: T, a: A, @@ -41,6 +60,13 @@ export function access>( return a(t); } +/** + * Accesses a value using an accessor constrained to return a specific type. + * Like access but only accepts accessors that return values of type R. + * @param t - The object to access + * @param a - The accessor constrained to return type R + * @returns The accessed value, typed as R + */ export function accessTo = AccessorTo>( t: T, a: A, diff --git a/packages/ctablex-core/src/accessor/fn-accessor.ts b/packages/ctablex-core/src/accessor/fn-accessor.ts index 496c909..8660eff 100644 --- a/packages/ctablex-core/src/accessor/fn-accessor.ts +++ b/packages/ctablex-core/src/accessor/fn-accessor.ts @@ -1,4 +1,10 @@ +/** + * A function that extracts a value from an object. + */ export type FnAccessor = (t: T) => R; +/** + * The return type of a function accessor. + */ export type FnAccessorValue> = F extends { (t: T, ...args: any[]): infer R; (t: T, ...args: any[]): any; @@ -41,6 +47,12 @@ export type FnAccessorValue> = F extends { ? R : any; +/** + * Accesses a value using a custom extraction function. + * @param obj - The object to access + * @param fn - Function that extracts the value + * @returns The result of calling fn with obj + */ export function accessByFn>( obj: T, fn: F, diff --git a/packages/ctablex-core/src/accessor/path-accessor.ts b/packages/ctablex-core/src/accessor/path-accessor.ts index bdd548f..8919dae 100644 --- a/packages/ctablex-core/src/accessor/path-accessor.ts +++ b/packages/ctablex-core/src/accessor/path-accessor.ts @@ -23,6 +23,11 @@ type AllowedIndexes< Tuple extends readonly [infer _, ...infer Tail] ? AllowedIndexes : Keys; +/** + * String literal type representing valid dot-separated paths through an object. + * Supports nested properties up to 5 levels deep. + * @example "user.address.city" + */ export type PathAccessor< T, TDepth extends any[] = [], @@ -46,6 +51,9 @@ type PathPrefix = TPrefix extends keyof T & ? `${TPrefix}.${PathAccessor & string}` : never; +/** + * The type of the value at a given path in an object. + */ export type PathAccessorValue = 0 extends 1 & T ? any : T extends null | undefined @@ -58,11 +66,22 @@ export type PathAccessorValue = 0 extends 1 & T : undefined : never; +/** + * String literal type representing paths through an object that return a specific type. + * Filters PathAccessor to only include paths where the value extends R. + */ export type PathAccessorTo = { [K in PathAccessor]: PathAccessorValue extends R ? K : never; }[PathAccessor] & string; +/** + * Accesses a nested property using a dot-separated string path. + * Provides full type safety with autocomplete and compile-time error detection. + * @param t - The object to access + * @param path - Dot-separated path like "user.address.city" + * @returns The value at the specified path + */ export function accessByPath>( t: T, path: K, @@ -71,6 +90,13 @@ export function accessByPath>( return path.split('.').reduce((acc, key) => acc?.[key], t); } +/** + * Accesses a nested property using a path constrained to return a specific type. + * Like accessByPath but only accepts paths that return values of type R. + * @param t - The object to access + * @param path - Dot-separated path that returns type R + * @returns The value at the specified path, typed as R + */ export function accessByPathTo< R, T, diff --git a/packages/ctablex-core/src/content-provider.tsx b/packages/ctablex-core/src/content-provider.tsx index 0fe5614..104c30f 100644 --- a/packages/ctablex-core/src/content-provider.tsx +++ b/packages/ctablex-core/src/content-provider.tsx @@ -1,6 +1,12 @@ import { ReactNode, useContext, useMemo } from 'react'; import { ContentContext } from './contexts/content-context'; +/** + * Retrieves the current value from the nearest ContentProvider. + * @param value - Optional override value. If provided, returns this value instead of context. + * @returns The content value from context or the override value. + * @throws Error if called outside a ContentProvider and no override value is provided. + */ export function useContent(value?: V) { const context = useContext(ContentContext); if (value !== undefined) { @@ -12,10 +18,19 @@ export function useContent(value?: V) { return context.value as V; } +/** + * Props for ContentProvider. + */ export interface ContentProviderProps { + /** The value to provide via context. */ value: V; children?: ReactNode; } + +/** + * Provides a content context that can be retrieved with useContent. + * Providers can be nested to create scoped contexts. + */ export function ContentProvider(props: ContentProviderProps) { const context = useMemo(() => ({ value: props.value }), [props.value]); return ( diff --git a/packages/ctablex-core/src/contents/array-content.tsx b/packages/ctablex-core/src/contents/array-content.tsx index 52acc6e..94ec4ce 100644 --- a/packages/ctablex-core/src/contents/array-content.tsx +++ b/packages/ctablex-core/src/contents/array-content.tsx @@ -4,17 +4,30 @@ import { ContentProvider, useContent } from '../content-provider'; import { IndexContext } from '../contexts/index-context'; import { DefaultContent } from './default-content'; +/** + * Function type for extracting a unique key from array elements. + */ export type ArrayGetKey = (value: V, index: number) => string | number; export interface ArrayContentProps { + /** Extracts unique key from each element (path or function). Defaults to index. */ getKey?: PathAccessorTo | ArrayGetKey; + /** Content to render for each element. Defaults to . */ children?: ReactNode; + /** Content to render between elements (e.g., commas, separators). */ join?: ReactNode; + /** Array to iterate. If omitted, uses context value. */ value?: ReadonlyArray; } const defaultChildren = ; +/** + * Iterates over an array, rendering children for each element. + * Provides both the array element via ContentProvider and its index via IndexContext. + * + * Default children: + */ export function ArrayContent(props: ArrayContentProps) { const { getKey: getKeyProps, diff --git a/packages/ctablex-core/src/contents/content-value.tsx b/packages/ctablex-core/src/contents/content-value.tsx index a8edda3..fd50252 100644 --- a/packages/ctablex-core/src/contents/content-value.tsx +++ b/packages/ctablex-core/src/contents/content-value.tsx @@ -10,6 +10,15 @@ export interface ContentValueProps { } const defaultChildren = ; +/** + * Transforms the content value using an accessor, then provides the result to children. + * - Path string: Accesses nested properties like "user.address.city" + * - Function: Calls the function with the content value + * - undefined: Returns the content value unchanged + * - null: Returns null + * + * Default children: + */ export function ContentValue(props: ContentValueProps) { const { accessor, children = defaultChildren } = props; const content = useContent(props.value); diff --git a/packages/ctablex-core/src/contents/default-content.tsx b/packages/ctablex-core/src/contents/default-content.tsx index e0e52a2..a94a49b 100644 --- a/packages/ctablex-core/src/contents/default-content.tsx +++ b/packages/ctablex-core/src/contents/default-content.tsx @@ -1,5 +1,10 @@ import { useContent } from '../content-provider'; +/** + * Renders primitive values (string, number, null, undefined) directly from context. + * Used as the default children for most content components. + * Only works with primitives - objects and arrays of objects will cause React errors. + */ export function DefaultContent() { const content = useContent(); return <>{content}; diff --git a/packages/ctablex-core/src/contents/empty-content.tsx b/packages/ctablex-core/src/contents/empty-content.tsx index 986d8e5..ff68a78 100644 --- a/packages/ctablex-core/src/contents/empty-content.tsx +++ b/packages/ctablex-core/src/contents/empty-content.tsx @@ -1,15 +1,31 @@ import { ReactNode } from 'react'; import { useContent } from '../content-provider'; +/** + * Props for the {@link EmptyContent} component. + */ export interface EmptyContentProps { + /** Content to render when the content is empty. */ children?: ReactNode; + /** Custom function to determine if content is empty. By default, only arrays with length 0 are considered empty. */ isEmpty?: (content: C) => boolean; } +/** + * Default implementation to check if content is empty. + * @param content - The content to check + * @returns `true` if content is an array with length 0, `false` otherwise + */ export function defaultIsEmpty(content: C): boolean { return Array.isArray(content) && content.length === 0; } +/** + * Renders its children only when the content is null, undefined, or empty. + * @remarks + * Uses {@link useContent} to access the current content from the context. + * By default, only arrays with length 0 are considered empty. + */ export function EmptyContent(props: EmptyContentProps) { const { children, isEmpty = defaultIsEmpty } = props; const content = useContent(); diff --git a/packages/ctablex-core/src/contents/field-content.tsx b/packages/ctablex-core/src/contents/field-content.tsx index 02dd2ee..19f7098 100644 --- a/packages/ctablex-core/src/contents/field-content.tsx +++ b/packages/ctablex-core/src/contents/field-content.tsx @@ -8,6 +8,12 @@ export interface FieldContentProps { } const defaultChildren = ; +/** + * Accesses a single field of an object and provides its value to children. + * Simplified version of AccessorContent for object properties. + * + * Default children: + */ export function FieldContent(props: FieldContentProps) { const { field, children = defaultChildren } = props; const content = useContent(); diff --git a/packages/ctablex-core/src/contents/index-content.tsx b/packages/ctablex-core/src/contents/index-content.tsx index 1dbd147..bb10c02 100644 --- a/packages/ctablex-core/src/contents/index-content.tsx +++ b/packages/ctablex-core/src/contents/index-content.tsx @@ -3,6 +3,10 @@ import { useIndex } from '../contexts/index-context'; export interface IndexContentProps { start?: number; } +/** + * Displays the current array or object iteration index from IndexContext. + * Optionally adds a start offset to the index. + */ export function IndexContent(props: IndexContentProps) { const { start = 0 } = props; const index = useIndex() + start; diff --git a/packages/ctablex-core/src/contents/key-content.tsx b/packages/ctablex-core/src/contents/key-content.tsx index 7b4308c..e677eb9 100644 --- a/packages/ctablex-core/src/contents/key-content.tsx +++ b/packages/ctablex-core/src/contents/key-content.tsx @@ -1,5 +1,8 @@ import { useKey } from '../contexts/key-context'; +/** + * Displays the current object property key from KeyContext. + */ export function KeyContent() { const key = useKey(); return <>{key}; diff --git a/packages/ctablex-core/src/contents/non-empty-content.tsx b/packages/ctablex-core/src/contents/non-empty-content.tsx index bf72dbf..9f90cb4 100644 --- a/packages/ctablex-core/src/contents/non-empty-content.tsx +++ b/packages/ctablex-core/src/contents/non-empty-content.tsx @@ -2,11 +2,22 @@ import { ReactNode } from 'react'; import { useContent } from '../content-provider'; import { defaultIsEmpty } from './empty-content'; +/** + * Props for the {@link NonEmptyContent} component. + */ export interface NonEmptyContentProps { + /** Content to render when the content is not empty. */ children?: ReactNode; + /** Custom function to determine if content is empty. By default, only arrays with length 0 are considered empty. */ isEmpty?: (content: C) => boolean; } +/** + * Renders its children only when the content is not null, not undefined, and not empty. + * @remarks + * Uses {@link useContent} to access the current content from the context. + * By default, only arrays with length 0 are considered empty. + */ export function NonEmptyContent(props: NonEmptyContentProps) { const { children, isEmpty = defaultIsEmpty } = props; const content = useContent(); diff --git a/packages/ctablex-core/src/contents/null-content.tsx b/packages/ctablex-core/src/contents/null-content.tsx index 99e1091..8f560a1 100644 --- a/packages/ctablex-core/src/contents/null-content.tsx +++ b/packages/ctablex-core/src/contents/null-content.tsx @@ -1,10 +1,19 @@ import { ReactNode } from 'react'; import { useContent } from '../content-provider'; +/** + * Props for the {@link NullContent} component. + */ export interface NullContentProps { + /** Content to render when the content is null or undefined. */ children?: ReactNode; } +/** + * Renders its children only when the content is null or undefined. + * @remarks + * Uses {@link useContent} to access the current content from the context. + */ export function NullContent(props: NullContentProps) { const { children } = props; const content = useContent(); diff --git a/packages/ctablex-core/src/contents/nullable-content.tsx b/packages/ctablex-core/src/contents/nullable-content.tsx index 3f0f4e4..dff8008 100644 --- a/packages/ctablex-core/src/contents/nullable-content.tsx +++ b/packages/ctablex-core/src/contents/nullable-content.tsx @@ -8,6 +8,12 @@ export interface NullableContentProps { } const defaultChildren = ; +/** + * Conditionally renders content based on whether the value is null or undefined. + * Renders nullContent when value is null/undefined, otherwise renders children. + * + * Default children: + */ export function NullableContent(props: NullableContentProps) { const { nullContent = null, children = defaultChildren } = props; const content = useContent(); diff --git a/packages/ctablex-core/src/contents/object-content.tsx b/packages/ctablex-core/src/contents/object-content.tsx index 8ff2156..becdd08 100644 --- a/packages/ctablex-core/src/contents/object-content.tsx +++ b/packages/ctablex-core/src/contents/object-content.tsx @@ -3,6 +3,9 @@ import { ContentProvider, useContent } from '../content-provider'; import { IndexContext } from '../contexts/index-context'; import { KeyContext } from '../contexts/key-context'; +/** + * Function type for generating React keys from object properties. + */ export type ObjectGetKey = ( value: V[K], key: K, @@ -10,14 +13,22 @@ export type ObjectGetKey = ( ) => string | number; export interface ObjectContentProps { + /** Generates unique React key for each property. Defaults to property key. */ getKey?: ObjectGetKey; + /** Content to render for each property. */ children: ReactNode; + /** Content to render between properties (e.g., commas, separators). */ join?: ReactNode; + /** Object to iterate. If omitted, uses context value. */ value?: V; } const defaultGetKey: ObjectGetKey = (value, key, index) => key.toString(); +/** + * Iterates over object properties, rendering children for each key-value pair. + * Provides the property value via ContentProvider, key via KeyContext, and index via IndexContext. + */ export function ObjectContent(props: ObjectContentProps) { const { getKey = defaultGetKey, children, join = null } = props; const content = useContent(props.value); diff --git a/packages/ctablex-core/src/contexts/content-context.tsx b/packages/ctablex-core/src/contexts/content-context.tsx index 547d4cb..5bc208e 100644 --- a/packages/ctablex-core/src/contexts/content-context.tsx +++ b/packages/ctablex-core/src/contexts/content-context.tsx @@ -1,6 +1,16 @@ import { createContext } from 'react'; +/** + * The type of the content context value. + * @internal This is an internal API and may change in future versions. + */ export type ContentContextType = { value: V }; + +/** + * The underlying React Context for the micro-context pattern. + * @internal This is an internal API and may change in future versions. + * Use `ContentProvider` and `useContent` instead. + */ export const ContentContext = createContext< ContentContextType | undefined >(undefined); diff --git a/packages/ctablex-core/src/contexts/index-context.tsx b/packages/ctablex-core/src/contexts/index-context.tsx index b5f829a..267615c 100644 --- a/packages/ctablex-core/src/contexts/index-context.tsx +++ b/packages/ctablex-core/src/contexts/index-context.tsx @@ -1,7 +1,16 @@ import { createContext, useContext } from 'react'; +/** + * Context providing the current array or object iteration index. + * Used internally by ArrayContent and ObjectContent. + */ export const IndexContext = createContext(undefined); +/** + * Retrieves the current iteration index from ArrayContent or ObjectContent. + * @returns The zero-based iteration index. + * @throws Error if called outside an ArrayContent or ObjectContent. + */ export function useIndex() { const context = useContext(IndexContext); if (context === undefined) { diff --git a/packages/ctablex-core/src/contexts/key-context.tsx b/packages/ctablex-core/src/contexts/key-context.tsx index 422f985..b63b7e6 100644 --- a/packages/ctablex-core/src/contexts/key-context.tsx +++ b/packages/ctablex-core/src/contexts/key-context.tsx @@ -1,9 +1,18 @@ import { createContext, useContext } from 'react'; +/** + * Context providing the current object property key during iteration. + * Used internally by ObjectContent. + */ export const KeyContext = createContext( undefined, ); +/** + * Retrieves the current object property key from ObjectContent. + * @returns The property key (string, number, or symbol). + * @throws Error if called outside an ObjectContent. + */ export function useKey() { const context = useContext(KeyContext); if (context === undefined) {