From 9447337b14ae170406334d75d9192f31b242b819 Mon Sep 17 00:00:00 2001 From: Seyyed Morteza Moosavi Date: Fri, 28 Nov 2025 16:47:57 +0330 Subject: [PATCH 1/7] docs: add micro-context doc --- packages/ctablex-core/MICRO-CONTEXT.md | 418 +++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 packages/ctablex-core/MICRO-CONTEXT.md diff --git a/packages/ctablex-core/MICRO-CONTEXT.md b/packages/ctablex-core/MICRO-CONTEXT.md new file mode 100644 index 0000000..f507514 --- /dev/null +++ b/packages/ctablex-core/MICRO-CONTEXT.md @@ -0,0 +1,418 @@ +# 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. + +## The Problem It Solves + +In traditional React patterns, data flows through props: + +```tsx + + + + + + +
+``` + +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: + +```tsx + + + {/* provides each item via context */} + + {/* gets value from context */} + + + +``` + +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 AccessorContent({ 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: + +- **AccessorContent** - 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... */} + + +``` + +## Real-World Example + +Building a table with micro-context: + +```tsx + + + + + + + + + + + + +``` + +Data flow: + +1. `DataTable` provides `products` array via context +2. `Table` iterates rows, providing each product via context +3. `Column` uses accessor to extract field, provides it via context +4. `BooleanContent`/`NumberContent` consume from context and render + +## Benefits + +### Decoupled Components + +Children don't need to know parent props or data structure: + +```tsx +// This component works anywhere +function PriceFormatter() { + const price = useContent(); + return ${price.toFixed(2)}; +} +``` + +### Flexible Composition + +Mix and match transformers and renderers: + +```tsx + + + + + + + + + +``` + +## Trade-offs and Limitations + +### No Strong Type Safety + +The biggest drawback of micro-context is the **lack of strong type safety**. While TypeScript can provide some validation, it requires manual intervention and is easily bypassed. + +#### The Problem + +**Generic types must be explicitly passed** to both content components and the `useContent` hook: + +```tsx +type User = { name: string; address: { city: string } }; +type Data = { user: User }; +const data: Data = ...; + + + {/* Must pass generic type for field/accessor validation */} + field="user"> {/* ✓ field is validated */} + field="name"> {/* ✓ field is validated */} + + + + + +// useContent also requires manual type annotation +function MyComponent() { + const user = useContent(); // ✗ Type is manual - no validation + return
{user.name}
; +} + +// Without generic type, TypeScript cannot validate fields + {/* ✗ no type checking */} + {/* ✗ no error! */} + + + + +// Wrong generic type also won't show errors + field="user"> {/* ✗ no error, but User is wrong type */} + {/* ✗ still no validation */} + + + +``` + +#### Why This Happens + +- JSX elements cannot be validated at compile time to infer generics +- TypeScript cannot verify that the generic type matches the actual context value +- `useContent()` type is purely manual - TypeScript cannot verify 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** - IDEs can't suggest valid field names without correct generics + +### Comparison with Props + +With traditional props, TypeScript provides strong guarantees: + +```tsx +// Props approach - strong type safety +interface CellProps { + value: number; +} +function Cell({ value }: CellProps) { + return <>{value.toFixed(2)}; // ✓ TypeScript knows value is number +} + +; // ✓ TypeScript validates price is number + +// Micro-context approach - weak type safety +function Cell() { + const value = useContent(); // ✗ Type is just a hint + return <>{value.toFixed(2)}; +} + + + {/* ✗ No validation that price is actually number */} +; +``` + +## 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 | + +## When to Use Micro-Context + +Micro-context excels when: + +- Building **repetitive structures** (tables, lists, forms) +- Creating **reusable renderers** that work with different data +- **Transforming data** through multiple steps +- Avoiding **prop drilling** in component trees +- Enabling **composition** over configuration + +Micro-context may be overkill for: + +- Simple one-off components +- Flat data structures with no transformation +- Cases where explicit props are clearer + +### When Type Safety Matters Most + +Consider avoiding micro-context when: + +- Working with complex, frequently-changing data structures +- Type safety is critical for correctness +- Team members are less familiar with the codebase +- Refactoring is frequent + +The flexibility and composition benefits may not outweigh the loss of compile-time guarantees. + +## 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. From 1db379478817056997dc56c2cabb861047c30a39 Mon Sep 17 00:00:00 2001 From: Seyyed Morteza Moosavi Date: Fri, 28 Nov 2025 17:20:08 +0330 Subject: [PATCH 2/7] docs: add docs for content context --- packages/ctablex-core/docs/ContentContext.md | 402 ++++++++++++++++++ packages/ctablex-core/index.d.ts | 23 + .../ctablex-core/src/content-provider.tsx | 15 + .../src/contexts/content-context.tsx | 10 + 4 files changed, 450 insertions(+) create mode 100644 packages/ctablex-core/docs/ContentContext.md diff --git a/packages/ctablex-core/docs/ContentContext.md b/packages/ctablex-core/docs/ContentContext.md new file mode 100644 index 0000000..19b609c --- /dev/null +++ b/packages/ctablex-core/docs/ContentContext.md @@ -0,0 +1,402 @@ +# 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 + +## 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
)} +
+``` + +### Why Value is Wrapped in Object + +The context type is `{ value: V }` instead of just `V`: + +```tsx +// Actual structure +type ContentContextType = { value: V }; + +// NOT this +type ContentContextType = V; +``` + +**Reason:** React Context uses reference equality to detect changes. Wrapping in an object ensures distinct contexts are properly detected, especially with primitives or multiple nested providers. + +## Type Safety Limitations + +### No Type Inference + +The hook cannot infer the type from context. You must manually specify it: + +```tsx +type User = { name: string }; + + + {/* Must manually specify type */} + const user = useContent(); + +``` + +### No Validation + +TypeScript cannot verify that the type parameter on `useContent` matches the actual context value: + +```tsx + + {/* Wrong type in useContent, but no compile error! */} + const user = useContent(); // ✗ Type mismatch, but compiles + return
{user.name}
; // ✗ 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](./FieldContent.md) - Access object fields +- [ArrayContent](./ArrayContent.md) - Map arrays +- [DefaultContent](./DefaultContent.md) - Render primitive values diff --git a/packages/ctablex-core/index.d.ts b/packages/ctablex-core/index.d.ts index 33b78f8..9d389a6 100644 --- a/packages/ctablex-core/index.d.ts +++ b/packages/ctablex-core/index.d.ts @@ -83,19 +83,36 @@ 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; } @@ -284,6 +301,12 @@ 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; export declare function useIndex(): number; 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/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); From 05e6fb038a4d4e081842e7fcbacfeb31f83fbfa2 Mon Sep 17 00:00:00 2001 From: Seyyed Morteza Moosavi Date: Fri, 28 Nov 2025 19:37:41 +0330 Subject: [PATCH 3/7] docs: add docs for accessors --- packages/ctablex-core/docs/Accessors.md | 548 ++++++++++++++++++ packages/ctablex-core/index.d.ts | 64 ++ .../ctablex-core/src/accessor/accessor.ts | 26 + .../ctablex-core/src/accessor/fn-accessor.ts | 12 + .../src/accessor/path-accessor.ts | 26 + 5 files changed, 676 insertions(+) create mode 100644 packages/ctablex-core/docs/Accessors.md diff --git a/packages/ctablex-core/docs/Accessors.md b/packages/ctablex-core/docs/Accessors.md new file mode 100644 index 0000000..dded9df --- /dev/null +++ b/packages/ctablex-core/docs/Accessors.md @@ -0,0 +1,548 @@ +# 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**. + +## 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"`, `"user.name"` +- 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 `AccessorContent` and other content components: + +```tsx +import { AccessorContent, 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 + +- [AccessorContent](./AccessorContent.md) - Component using accessors +- [FieldContent](./FieldContent.md) - 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/index.d.ts b/packages/ctablex-core/index.d.ts index 9d389a6..729f1f9 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, @@ -149,8 +195,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; @@ -258,6 +310,11 @@ declare type ObjectGetKey = ( 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[] = [], @@ -276,11 +333,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 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, From 4b0b2e44699380239a8024551c8e13bb876f858f Mon Sep 17 00:00:00 2001 From: Seyyed Morteza Moosavi Date: Fri, 28 Nov 2025 20:51:28 +0330 Subject: [PATCH 4/7] docs: add docs for contents --- packages/ctablex-core/docs/Accessors.md | 4 +- packages/ctablex-core/docs/ArrayContent.md | 233 ++++++++++ packages/ctablex-core/docs/ContentContext.md | 4 +- packages/ctablex-core/docs/Contents.md | 427 ++++++++++++++++++ packages/ctablex-core/docs/ObjectContent.md | 235 ++++++++++ packages/ctablex-core/index.d.ts | 75 +++ .../src/contents/array-content.tsx | 13 + .../src/contents/content-value.tsx | 9 + .../src/contents/default-content.tsx | 5 + .../src/contents/field-content.tsx | 6 + .../src/contents/index-content.tsx | 4 + .../ctablex-core/src/contents/key-content.tsx | 3 + .../src/contents/nullable-content.tsx | 6 + .../src/contents/object-content.tsx | 11 + .../src/contexts/index-context.tsx | 9 + .../ctablex-core/src/contexts/key-context.tsx | 9 + 16 files changed, 1049 insertions(+), 4 deletions(-) create mode 100644 packages/ctablex-core/docs/ArrayContent.md create mode 100644 packages/ctablex-core/docs/Contents.md create mode 100644 packages/ctablex-core/docs/ObjectContent.md diff --git a/packages/ctablex-core/docs/Accessors.md b/packages/ctablex-core/docs/Accessors.md index dded9df..a769924 100644 --- a/packages/ctablex-core/docs/Accessors.md +++ b/packages/ctablex-core/docs/Accessors.md @@ -542,7 +542,7 @@ type User = { ## Related -- [AccessorContent](./AccessorContent.md) - Component using accessors -- [FieldContent](./FieldContent.md) - Simplified object field access +- [AccessorContent](./Contents.md#accessorcontent) - 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..859d04a --- /dev/null +++ b/packages/ctablex-core/docs/ArrayContent.md @@ -0,0 +1,233 @@ +# Array Content Components + +Components for iterating over arrays and accessing array indices. + +## 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 +- Wraps each element in a `ContentProvider` with the element value +- Provides the array index via `IndexContext` +- 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 +``` + +## 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 index 19b609c..a6d94dd 100644 --- a/packages/ctablex-core/docs/ContentContext.md +++ b/packages/ctablex-core/docs/ContentContext.md @@ -397,6 +397,6 @@ function UserGreeting({ name }: { name?: string }) { ## Related - [Micro-Context Pattern](../MICRO-CONTEXT.md) - Pattern overview -- [FieldContent](./FieldContent.md) - Access object fields +- [FieldContent](./Contents.md#fieldcontent) - Access object fields - [ArrayContent](./ArrayContent.md) - Map arrays -- [DefaultContent](./DefaultContent.md) - Render primitive values +- [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..850bd44 --- /dev/null +++ b/packages/ctablex-core/docs/Contents.md @@ -0,0 +1,427 @@ +# Content Components + +Utility components for accessing fields, transforming values, and handling null/undefined. + +## AccessorContent + +Transforms the content value using an accessor (path, function, undefined, or null), then provides the result to children. + +### Props + +```tsx +interface AccessorContentProps { + 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 { AccessorContent, 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: true */} + + + +// 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 `AccessorContent` 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 AccessorContent + +`FieldContent` is simpler but less flexible: + +```tsx +// FieldContent - simple, direct property access + + + + +// AccessorContent - 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 + +## 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. + +## 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/ObjectContent.md b/packages/ctablex-core/docs/ObjectContent.md new file mode 100644 index 0000000..8f0d361 --- /dev/null +++ b/packages/ctablex-core/docs/ObjectContent.md @@ -0,0 +1,235 @@ +# Object Content Components + +Components for iterating over object properties and accessing object keys. + +## 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 +- Wraps each property value in a `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/index.d.ts b/packages/ctablex-core/index.d.ts index 729f1f9..df99ebd 100644 --- a/packages/ctablex-core/index.d.ts +++ b/packages/ctablex-core/index.d.ts @@ -109,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< @@ -163,6 +176,15 @@ export declare interface ContentProviderProps { 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 }; @@ -175,6 +197,11 @@ 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; export declare function EmptyContent( @@ -186,6 +213,12 @@ export declare interface EmptyContentProps { 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; @@ -247,12 +280,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[] & { @@ -263,8 +304,15 @@ 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; export declare function NonEmptyContent( @@ -276,6 +324,12 @@ export declare interface NonEmptyContentProps { 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; @@ -293,17 +347,28 @@ export declare interface NullContentProps { 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, @@ -373,8 +438,18 @@ declare type PathPrefix< */ 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/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/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/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/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) { From aed35dcb08e208de81db1bc693466e27319b2e29 Mon Sep 17 00:00:00 2001 From: Seyyed Morteza Moosavi Date: Fri, 28 Nov 2025 22:17:16 +0330 Subject: [PATCH 5/7] docs: add readme.md --- packages/ctablex-core/README.md | 229 ++++++++++++++++ packages/ctablex-core/docs/Accessors.md | 2 +- packages/ctablex-core/docs/ArrayContent.md | 2 +- packages/ctablex-core/docs/ContentContext.md | 2 +- packages/ctablex-core/docs/Contents.md | 2 +- .../ctablex-core/{ => docs}/MICRO-CONTEXT.md | 2 +- packages/ctablex-core/docs/ObjectContent.md | 2 +- packages/ctablex-core/docs/README.md | 257 ++++++++++++++++++ packages/ctablex-core/package.json | 22 +- 9 files changed, 513 insertions(+), 7 deletions(-) create mode 100644 packages/ctablex-core/README.md rename packages/ctablex-core/{ => docs}/MICRO-CONTEXT.md (99%) create mode 100644 packages/ctablex-core/docs/README.md diff --git a/packages/ctablex-core/README.md b/packages/ctablex-core/README.md new file mode 100644 index 0000000..b05dd61 --- /dev/null +++ b/packages/ctablex-core/README.md @@ -0,0 +1,229 @@ +# @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 + +## 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}
; +} +``` + +### Content Components + +Transform and access data through context: + +```tsx +import { + AccessorContent, + FieldContent, + ArrayContent, + ObjectContent, + NullableContent, + DefaultContent, +} from '@ctablex/core'; + +// Access nested paths + + + + +// Access object fields + + + + +// Iterate arrays + + + + + + + + +// Iterate objects + + + : + + + +// Handle null/undefined + + + + + +``` + +### Type-Safe Accessors + +Extract values with strong TypeScript support: + +```tsx +import { accessByPath, access } from '@ctablex/core'; + +type User = { + profile: { + name: string; + }; +}; + +const user: User = { profile: { name: 'Bob' } }; + +// Path accessor with autocomplete +const name = accessByPath(user, 'profile.name'); // ✓ Type-safe + +// Function accessor +const value = access(user, (u) => u.profile.name); // ✓ Type inference +``` + +## 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 + +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 + +⚠️ **Important:** Generic types must be explicitly provided for type checking and autocomplete: + +```tsx +// ✓ Type-safe with explicit generic + field="name"> + + + +// ✗ No type checking without generic + + + + +// ✓ Accessor props are validated based on generic + accessor="profile.name"> + +
+``` + +**Note:** Nested components do NOT inherit types automatically - each needs its own explicit generic for type safety. + +## 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)** - AccessorContent, 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 index a769924..8b114f0 100644 --- a/packages/ctablex-core/docs/Accessors.md +++ b/packages/ctablex-core/docs/Accessors.md @@ -545,4 +545,4 @@ type User = { - [AccessorContent](./Contents.md#accessorcontent) - 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 +- [Micro-Context Pattern](./MICRO-CONTEXT.md) - Pattern overview diff --git a/packages/ctablex-core/docs/ArrayContent.md b/packages/ctablex-core/docs/ArrayContent.md index 859d04a..19c504c 100644 --- a/packages/ctablex-core/docs/ArrayContent.md +++ b/packages/ctablex-core/docs/ArrayContent.md @@ -230,4 +230,4 @@ function CustomIndexDisplay() { - [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 +- [Micro-Context Pattern](./MICRO-CONTEXT.md) - Pattern overview diff --git a/packages/ctablex-core/docs/ContentContext.md b/packages/ctablex-core/docs/ContentContext.md index a6d94dd..0c39754 100644 --- a/packages/ctablex-core/docs/ContentContext.md +++ b/packages/ctablex-core/docs/ContentContext.md @@ -396,7 +396,7 @@ function UserGreeting({ name }: { name?: string }) { ## Related -- [Micro-Context Pattern](../MICRO-CONTEXT.md) - Pattern overview +- [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 index 850bd44..d6218a6 100644 --- a/packages/ctablex-core/docs/Contents.md +++ b/packages/ctablex-core/docs/Contents.md @@ -424,4 +424,4 @@ Many content components use `` as default children. This cause - [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 +- [Micro-Context Pattern](./MICRO-CONTEXT.md) - Pattern overview diff --git a/packages/ctablex-core/MICRO-CONTEXT.md b/packages/ctablex-core/docs/MICRO-CONTEXT.md similarity index 99% rename from packages/ctablex-core/MICRO-CONTEXT.md rename to packages/ctablex-core/docs/MICRO-CONTEXT.md index f507514..a21c1d2 100644 --- a/packages/ctablex-core/MICRO-CONTEXT.md +++ b/packages/ctablex-core/docs/MICRO-CONTEXT.md @@ -227,7 +227,7 @@ Beyond value context, micro-contexts can provide metadata: ## Real-World Example -Building a table with micro-context: +Building a table with micro-context (using `@ctablex/table`, which is built on `@ctablex/core`): ```tsx diff --git a/packages/ctablex-core/docs/ObjectContent.md b/packages/ctablex-core/docs/ObjectContent.md index 8f0d361..e15af5f 100644 --- a/packages/ctablex-core/docs/ObjectContent.md +++ b/packages/ctablex-core/docs/ObjectContent.md @@ -232,4 +232,4 @@ const settings = { - [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 +- [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..16efdb8 --- /dev/null +++ b/packages/ctablex-core/docs/README.md @@ -0,0 +1,257 @@ +# 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: + +- **AccessorContent** - Transform values using paths or functions +- **FieldContent** - Access object properties +- **NullableContent** - 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 + +#### [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#accessorcontent) and [Accessors](./Accessors.md) + +### Conditional Rendering + +```tsx + + + + + + + +``` + +Start with: [Contents](./Contents.md#nullablecontent) + +## Type Safety + +### Important Understanding + +TypeScript type safety in ctablex has specific characteristics: + +1. **Generic types on components are NOT validated** - You can pass wrong types +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 + +Example: + +```tsx +// ✓ getKey is type-safe with autocomplete + getKey="id"> + {/* ✗ No automatic type safety for nested components */} + + + +// ✓ Add explicit generic for type safety + getKey="id"> + field="name" /> + +``` + +Read more: [MICRO-CONTEXT.md - Trade-offs](./MICRO-CONTEXT.md#trade-offs) + +## 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)** - AccessorContent, 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/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", From e0f4b5cd398a13d852c979bbeaf1df1bd388418f Mon Sep 17 00:00:00 2001 From: Seyyed Morteza Moosavi Date: Fri, 12 Dec 2025 01:13:38 +0330 Subject: [PATCH 6/7] docs: add null-content, empty-contnet and non-empty-content docs --- packages/ctablex-core/docs/ArrayContent.md | 226 ++++++++++++++++++ packages/ctablex-core/docs/Contents.md | 87 +++++++ packages/ctablex-core/index.d.ts | 31 +++ .../src/contents/empty-content.tsx | 16 ++ .../src/contents/non-empty-content.tsx | 11 + .../src/contents/null-content.tsx | 9 + 6 files changed, 380 insertions(+) diff --git a/packages/ctablex-core/docs/ArrayContent.md b/packages/ctablex-core/docs/ArrayContent.md index 19c504c..8cfce4d 100644 --- a/packages/ctablex-core/docs/ArrayContent.md +++ b/packages/ctablex-core/docs/ArrayContent.md @@ -225,6 +225,232 @@ function CustomIndexDisplay() { // 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 (iterating over an empty array returns an empty array, so use `NonEmptyContent` when you need a wrapper) +- 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
+
+ + +
+ +
+
+
+
+``` + ## Related - [ObjectContent](./ObjectContent.md) - Iterate over object properties diff --git a/packages/ctablex-core/docs/Contents.md b/packages/ctablex-core/docs/Contents.md index d6218a6..4d3b441 100644 --- a/packages/ctablex-core/docs/Contents.md +++ b/packages/ctablex-core/docs/Contents.md @@ -307,6 +307,93 @@ const user2: User = { name: 'Bob', email: undefined }; - 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 +``` + ## DefaultContent Renders primitive values (string, number, null, undefined) directly. diff --git a/packages/ctablex-core/index.d.ts b/packages/ctablex-core/index.d.ts index df99ebd..36c0d25 100644 --- a/packages/ctablex-core/index.d.ts +++ b/packages/ctablex-core/index.d.ts @@ -204,12 +204,23 @@ export { ContentValueProps }; */ 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, uses {@link defaultIsEmpty} to check for empty arrays. + */ 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. Defaults to {@link defaultIsEmpty}. */ isEmpty?: (content: C) => boolean; } @@ -315,12 +326,23 @@ export declare function KeyContent(): JSX_2.Element; */ 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, uses {@link defaultIsEmpty} to check for empty arrays. + */ 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. Defaults to {@link defaultIsEmpty}. */ isEmpty?: (content: C) => boolean; } @@ -339,11 +361,20 @@ 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; } diff --git a/packages/ctablex-core/src/contents/empty-content.tsx b/packages/ctablex-core/src/contents/empty-content.tsx index 986d8e5..fc77c59 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. Defaults to {@link defaultIsEmpty}. */ 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, uses {@link defaultIsEmpty} to check for empty arrays. + */ export function EmptyContent(props: EmptyContentProps) { const { children, isEmpty = defaultIsEmpty } = props; const content = useContent(); diff --git a/packages/ctablex-core/src/contents/non-empty-content.tsx b/packages/ctablex-core/src/contents/non-empty-content.tsx index bf72dbf..8e16dea 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. Defaults to {@link defaultIsEmpty}. */ 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, uses {@link defaultIsEmpty} to check for empty arrays. + */ 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(); From 3c9f9e9cc77df3a3f5b9cb5d4e4db31b86aff6ae Mon Sep 17 00:00:00 2001 From: Seyyed Morteza Moosavi Date: Sat, 13 Dec 2025 09:20:46 +0330 Subject: [PATCH 7/7] docs: fix docs --- packages/ctablex-core/README.md | 48 ++- packages/ctablex-core/docs/Accessors.md | 30 +- packages/ctablex-core/docs/ArrayContent.md | 13 +- packages/ctablex-core/docs/ContentContext.md | 43 +-- packages/ctablex-core/docs/Contents.md | 296 ++++++++++-------- packages/ctablex-core/docs/MICRO-CONTEXT.md | 236 +++++--------- packages/ctablex-core/docs/ObjectContent.md | 8 +- packages/ctablex-core/docs/README.md | 44 +-- packages/ctablex-core/index.d.ts | 8 +- .../src/contents/empty-content.tsx | 4 +- .../src/contents/non-empty-content.tsx | 4 +- 11 files changed, 320 insertions(+), 414 deletions(-) diff --git a/packages/ctablex-core/README.md b/packages/ctablex-core/README.md index b05dd61..cf637b9 100644 --- a/packages/ctablex-core/README.md +++ b/packages/ctablex-core/README.md @@ -13,6 +13,8 @@ This enables: - **No prop drilling** through intermediate components - **Immutable children** for better performance optimization +Read more: [Micro-Context Pattern](./docs/MICRO-CONTEXT.md) + ## Installation ```bash @@ -65,24 +67,27 @@ function MyComponent() { } ``` +Read more: [ContentContext - ContentProvider & useContent](./docs/ContentContext.md) + ### Content Components Transform and access data through context: ```tsx import { - AccessorContent, + ContentValue, FieldContent, ArrayContent, ObjectContent, NullableContent, DefaultContent, + KeyContent, } from '@ctablex/core'; // Access nested paths - + - + // Access object fields @@ -113,12 +118,14 @@ import { ``` +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 { accessByPath, access } from '@ctablex/core'; +import { access } from '@ctablex/core'; type User = { profile: { @@ -129,12 +136,14 @@ type User = { const user: User = { profile: { name: 'Bob' } }; // Path accessor with autocomplete -const name = accessByPath(user, 'profile.name'); // ✓ Type-safe +const name = access(user, 'profile.name'); // ✓ Type-safe + autocomplete // Function accessor -const value = access(user, (u) => u.profile.name); // ✓ Type inference +const value = access(user, (u) => u.profile.name); // ✓ Type inference + auto type safety ``` +Read more: [Accessors](./docs/Accessors.md) + ## Key Features ### Reusable Renderers @@ -153,7 +162,7 @@ function PriceDisplay() { ; ``` -### Default Children +### Default Children and Open for Customization Components provide sensible defaults while remaining customizable: @@ -185,28 +194,9 @@ function ProductList() { } ``` -## Type Safety - -⚠️ **Important:** Generic types must be explicitly provided for type checking and autocomplete: - -```tsx -// ✓ Type-safe with explicit generic - field="name"> - -
- -// ✗ No type checking without generic - - - - -// ✓ Accessor props are validated based on generic - accessor="profile.name"> - - -``` +## Type Safety Limitations -**Note:** Nested components do NOT inherit types automatically - each needs its own explicit generic for type safety. +⚠️ 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 @@ -215,7 +205,7 @@ 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)** - AccessorContent, FieldContent, NullableContent, DefaultContent +- **[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 diff --git a/packages/ctablex-core/docs/Accessors.md b/packages/ctablex-core/docs/Accessors.md index 8b114f0..1466ac5 100644 --- a/packages/ctablex-core/docs/Accessors.md +++ b/packages/ctablex-core/docs/Accessors.md @@ -2,6 +2,8 @@ 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: @@ -115,7 +117,7 @@ type PathAccessor = /* ... */; Supports: -- Object properties: `"user"`, `"user.name"` +- Object properties: `"user"`, `"address"` - Nested paths: `"user.address.city"` (up to 5 levels deep) - Array indices for tuples: `tuple.0`, `tuple.1` @@ -463,13 +465,13 @@ type User = { 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.city"> {/* ✓ TypeScript validates "address.city" exists on User */} - + - accessor="address.country"> {/* ✗ Error! "country" doesn't exist on User */} + accessor="address.country"> {/* ✗ Error! "country" doesn't exist on User */} - + ``` **Key difference:** @@ -512,10 +514,10 @@ access(arr, (a) => a[0]); // Use function instead ## Usage with Content Components -Accessors are commonly used with `AccessorContent` and other content components: +Accessors are commonly used with `ContentValue` and other content components: ```tsx -import { AccessorContent, FieldContent } from '@ctablex/core'; +import { ContentValue, FieldContent } from '@ctablex/core'; type User = { profile: { @@ -525,24 +527,24 @@ type User = { }; // Path accessor - accessor="profile.name"> + accessor="profile.name"> - + // Function accessor - accessor={(user) => user.profile.age > 18}> + accessor={(user) => user.profile.age > 18}> - + // Undefined accessor (returns whole object) - accessor={undefined}> + accessor={undefined}> ... - + ``` ## Related -- [AccessorContent](./Contents.md#accessorcontent) - Component using accessors +- [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 index 8cfce4d..737b970 100644 --- a/packages/ctablex-core/docs/ArrayContent.md +++ b/packages/ctablex-core/docs/ArrayContent.md @@ -2,6 +2,14 @@ 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. @@ -29,8 +37,8 @@ type ArrayGetKey = (value: V, index: number) => string | number; ### Behavior - Iterates over the array from the content context -- Wraps each element in a `ContentProvider` with the element value - 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 @@ -394,7 +402,7 @@ import { NonEmptyContent, ContentProvider } from '@ctablex/core'; ### Use Cases - Display content only when data is available -- Wrap arrays with DOM elements only when not empty (iterating over an empty array returns an empty array, so use `NonEmptyContent` when you need a wrapper) +- Wrap arrays with DOM elements only when not empty - Apply custom `isEmpty` logic for different data types #### Wrapping Arrays with DOM Elements @@ -442,6 +450,7 @@ Use both components together to handle all cases:
No results found
+

Results:

diff --git a/packages/ctablex-core/docs/ContentContext.md b/packages/ctablex-core/docs/ContentContext.md index 0c39754..abca0e8 100644 --- a/packages/ctablex-core/docs/ContentContext.md +++ b/packages/ctablex-core/docs/ContentContext.md @@ -6,6 +6,10 @@ The Content Context system provides the foundation for the micro-context pattern - **`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 @@ -253,44 +257,29 @@ Legacy consumer pattern instead of hooks: ``` -### Why Value is Wrapped in Object - -The context type is `{ value: V }` instead of just `V`: - -```tsx -// Actual structure -type ContentContextType = { value: V }; - -// NOT this -type ContentContextType = V; -``` - -**Reason:** React Context uses reference equality to detect changes. Wrapping in an object ensures distinct contexts are properly detected, especially with primitives or multiple nested providers. - ## Type Safety Limitations -### No Type Inference +### No Type Inference or Validation -The hook cannot infer the type from context. You must manually specify it: +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 }; - - {/* Must manually specify type */} +function Component() { + // Must manually specify type - no inference const user = useContent(); - -``` - -### No Validation + return
{user.name}
; +} -TypeScript cannot verify that the type parameter on `useContent` matches the actual context value: +// ✓ Correct usage + + + -```tsx +// ✗ Type mismatch, but compiles! Runtime error! - {/* Wrong type in useContent, but no compile error! */} - const user = useContent(); // ✗ Type mismatch, but compiles - return
{user.name}
; // ✗ Runtime error! +
``` diff --git a/packages/ctablex-core/docs/Contents.md b/packages/ctablex-core/docs/Contents.md index 4d3b441..888a1f4 100644 --- a/packages/ctablex-core/docs/Contents.md +++ b/packages/ctablex-core/docs/Contents.md @@ -2,14 +2,149 @@ Utility components for accessing fields, transforming values, and handling null/undefined. -## AccessorContent +## 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 AccessorContentProps { +interface ContentValueProps { accessor: Accessor; children?: ReactNode; value?: V; @@ -33,7 +168,7 @@ interface AccessorContentProps { ### Example ```tsx -import { AccessorContent, DefaultContent } from '@ctablex/core'; +import { ContentValue, DefaultContent } from '@ctablex/core'; type User = { profile: { @@ -50,34 +185,34 @@ const user: User = { // Path accessor - + {/* Renders: Alice */} - +
// Function accessor - u.profile.age > 18}> - {/* Renders: true */} - + u.profile.age > 18}> + {/* Renders: Yes */} + // Undefined accessor (returns unchanged) - + {/* Renders: true */} - + // Null accessor - + - + // Renders: No value ``` @@ -96,26 +231,26 @@ type Product = { }; // ✓ Type-safe paths with autocomplete - accessor="metadata.weight"> + accessor="metadata.weight"> - + // ✗ Compile error - invalid path - accessor="metadata.height"> + accessor="metadata.height"> - + // ✓ Type-safe function - accessor={(p) => p.price * 1.1}> + 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 `AccessorContent` for object properties. +Accesses a single field of an object and provides its value to children. Simplified version of `ContentValue` for object properties. ### Props @@ -176,8 +311,8 @@ const user: User = { // Multiple fields - Name: , - Email: + Name: + Email: // Renders: Name: Bob, Email: bob@example.com @@ -222,7 +357,7 @@ type Product = { ``` -### Comparison with AccessorContent +### Comparison with ContentValue `FieldContent` is simpler but less flexible: @@ -232,10 +367,10 @@ type Product = { -// AccessorContent - more flexible, supports nested paths - +// ContentValue - more flexible, supports nested paths + - + ``` ## NullableContent @@ -394,117 +529,6 @@ const user: User = { name: 'Alice' }; // Renders: Email not provided ``` -## 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. - ## Related - [ArrayContent](./ArrayContent.md) - Iterate over arrays diff --git a/packages/ctablex-core/docs/MICRO-CONTEXT.md b/packages/ctablex-core/docs/MICRO-CONTEXT.md index a21c1d2..b7ffc7c 100644 --- a/packages/ctablex-core/docs/MICRO-CONTEXT.md +++ b/packages/ctablex-core/docs/MICRO-CONTEXT.md @@ -4,17 +4,21 @@ **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: +In traditional React patterns, data flows through props manually: ```tsx -
- - - - - +
+ {data.map((item) => ( + + + + + + ))}
``` @@ -26,19 +30,29 @@ This leads to: ## The Micro-Context Solution -Instead of passing data as props, wrap it in a context provider: +Instead of passing data as props, wrap it in a context provider, no data passed via props or manual iteration: ```tsx - - {/* provides each item via context */} - - {/* gets value from context */} - - + + {/* 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 @@ -119,16 +133,16 @@ Components can provide sensible defaults, reducing boilerplate: ```tsx // With default children - + // Equivalent to - + - + // Implementation const defaultChildren = ; -function AccessorContent({ accessor, children = defaultChildren }) { +function ContentValue({ accessor, children = defaultChildren }) { const content = useContent(); return ( @@ -188,7 +202,7 @@ function MyComponent() { Components that transform data and provide it via context: -- **AccessorContent** - Extract data using path or function accessors +- **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 @@ -201,14 +215,14 @@ Extract values from data structures: ```tsx // Path accessor - string-based - + - + // Function accessor - user.fullName}> + user.fullName}> - + ``` ### Additional Contexts @@ -225,113 +239,54 @@ Beyond value context, micro-contexts can provide metadata: ``` -## Real-World Example - -Building a table with micro-context (using `@ctablex/table`, which is built on `@ctablex/core`): - -```tsx - - - - - - - - - - - - -``` - -Data flow: - -1. `DataTable` provides `products` array via context -2. `Table` iterates rows, providing each product via context -3. `Column` uses accessor to extract field, provides it via context -4. `BooleanContent`/`NumberContent` consume from context and render +## Trade-offs and Limitations -## Benefits +### Weak Type Safety -### Decoupled Components +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. -Children don't need to know parent props or data structure: +#### How It Works -```tsx -// This component works anywhere -function PriceFormatter() { - const price = useContent(); - return ${price.toFixed(2)}; -} -``` +TypeScript type safety in ctablex has three key characteristics: -### Flexible Composition +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 -Mix and match transformers and renderers: +This means **generic types must be explicitly passed** to both content components and the `useContent` hook: ```tsx - - - - - - - - - -``` - -## Trade-offs and Limitations - -### No Strong Type Safety - -The biggest drawback of micro-context is the **lack of strong type safety**. While TypeScript can provide some validation, it requires manual intervention and is easily bypassed. - -#### The Problem +type User = { name: string; address: { city: string } }; -**Generic types must be explicitly passed** to both content components and the `useContent` hook: +// ✓ Type-safe with explicit generic - field is validated and autocompleted + field="name"> + + -```tsx -type User = { name: string; address: { city: string } }; -type Data = { user: User }; -const data: Data = ...; +// ✗ No type checking without generic - any field accepted + + + - - {/* Must pass generic type for field/accessor validation */} - field="user"> {/* ✓ field is validated */} - field="name"> {/* ✓ field is validated */} - - - - +// ✓ 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 manual - no validation + const user = useContent(); // ✗ Type is just a hint - not validated return
{user.name}
; } - -// Without generic type, TypeScript cannot validate fields - {/* ✗ no type checking */} - {/* ✗ no error! */} - - - - -// Wrong generic type also won't show errors - field="user"> {/* ✗ no error, but User is wrong type */} - {/* ✗ still no validation */} - - - ``` #### Why This Happens -- JSX elements cannot be validated at compile time to infer generics -- TypeScript cannot verify that the generic type matches the actual context value -- `useContent()` type is purely manual - TypeScript cannot verify it matches context -- Type safety depends entirely on developer discipline +- **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 @@ -339,33 +294,15 @@ function MyComponent() { - **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** - IDEs can't suggest valid field names without correct generics - -### Comparison with Props - -With traditional props, TypeScript provides strong guarantees: - -```tsx -// Props approach - strong type safety -interface CellProps { - value: number; -} -function Cell({ value }: CellProps) { - return <>{value.toFixed(2)}; // ✓ TypeScript knows value is number -} +- **No autocomplete without generics** - IDEs can't suggest valid field names -; // ✓ TypeScript validates price is number +## When to Use Micro-Context -// Micro-context approach - weak type safety -function Cell() { - const value = useContent(); // ✗ Type is just a hint - return <>{value.toFixed(2)}; -} +Micro-context excels when: - - {/* ✗ No validation that price is actually number */} -; -``` +- 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 @@ -378,33 +315,6 @@ function Cell() { | Nesting | Typically 1-2 levels | Multiple nested levels | | Examples | ThemeProvider, AuthProvider | ContentProvider, ArrayContent | -## When to Use Micro-Context - -Micro-context excels when: - -- Building **repetitive structures** (tables, lists, forms) -- Creating **reusable renderers** that work with different data -- **Transforming data** through multiple steps -- Avoiding **prop drilling** in component trees -- Enabling **composition** over configuration - -Micro-context may be overkill for: - -- Simple one-off components -- Flat data structures with no transformation -- Cases where explicit props are clearer - -### When Type Safety Matters Most - -Consider avoiding micro-context when: - -- Working with complex, frequently-changing data structures -- Type safety is critical for correctness -- Team members are less familiar with the codebase -- Refactoring is frequent - -The flexibility and composition benefits may not outweigh the loss of compile-time guarantees. - ## Summary Micro-context is a pattern for **localized, granular data flow** using React Context. By creating small, scoped providers within component trees, it enables: @@ -413,6 +323,4 @@ Micro-context is a pattern for **localized, granular data flow** using React Con - 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. +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 index e15af5f..148d20a 100644 --- a/packages/ctablex-core/docs/ObjectContent.md +++ b/packages/ctablex-core/docs/ObjectContent.md @@ -2,6 +2,12 @@ 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. @@ -31,7 +37,7 @@ type ObjectGetKey = ( ### Behavior - Iterates over `Object.keys()` of the object from content context -- Wraps each property value in a `ContentProvider` +- 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) diff --git a/packages/ctablex-core/docs/README.md b/packages/ctablex-core/docs/README.md index 16efdb8..ede4025 100644 --- a/packages/ctablex-core/docs/README.md +++ b/packages/ctablex-core/docs/README.md @@ -41,9 +41,9 @@ Components for transforming and accessing data: Utility components for common operations: -- **AccessorContent** - Transform values using paths or functions +- **ContentValue** - Transform values using paths or functions - **FieldContent** - Access object properties -- **NullableContent** - Handle null/undefined values +- **NullableContent**, **NullContent** - Handle null/undefined values - **DefaultContent** - Render primitive values #### [ArrayContent](./ArrayContent.md) @@ -53,6 +53,7 @@ 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) @@ -117,13 +118,13 @@ Start with: [ObjectContent](./ObjectContent.md) ```tsx - + - + ``` -Start with: [Contents](./Contents.md#accessorcontent) and [Accessors](./Accessors.md) +Start with: [Contents](./Contents.md#contentvalue) and [Accessors](./Accessors.md) ### Conditional Rendering @@ -139,32 +140,9 @@ Start with: [Contents](./Contents.md#accessorcontent) and [Accessors](./Accessor Start with: [Contents](./Contents.md#nullablecontent) -## Type Safety +## Type Safety Limitations -### Important Understanding - -TypeScript type safety in ctablex has specific characteristics: - -1. **Generic types on components are NOT validated** - You can pass wrong types -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 - -Example: - -```tsx -// ✓ getKey is type-safe with autocomplete - getKey="id"> - {/* ✗ No automatic type safety for nested components */} - - - -// ✓ Add explicit generic for type safety - getKey="id"> - field="name" /> - -``` - -Read more: [MICRO-CONTEXT.md - Trade-offs](./MICRO-CONTEXT.md#trade-offs) +⚠️ 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 @@ -231,10 +209,10 @@ Read more: Each component's Type Safety section ```tsx // ✗ Too deep - + // ✓ Use function accessor instead - obj.a.b.c.d.e.f} /> + obj.a.b.c.d.e.f} /> ``` Read more: [Accessors - Limitations](./Accessors.md#limitations) @@ -243,7 +221,7 @@ Read more: [Accessors - Limitations](./Accessors.md#limitations) - **[MICRO-CONTEXT.md](./MICRO-CONTEXT.md)** - Core pattern explanation - **[ContentContext.md](./ContentContext.md)** - ContentProvider, useContent, ContentContext -- **[Contents.md](./Contents.md)** - AccessorContent, FieldContent, NullableContent, DefaultContent +- **[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 diff --git a/packages/ctablex-core/index.d.ts b/packages/ctablex-core/index.d.ts index 36c0d25..ad4593e 100644 --- a/packages/ctablex-core/index.d.ts +++ b/packages/ctablex-core/index.d.ts @@ -208,7 +208,7 @@ 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, uses {@link defaultIsEmpty} to check for empty arrays. + * By default, only arrays with length 0 are considered empty. */ export declare function EmptyContent( props: EmptyContentProps, @@ -220,7 +220,7 @@ export declare function EmptyContent( export declare interface EmptyContentProps { /** Content to render when the content is empty. */ children?: ReactNode; - /** Custom function to determine if content is empty. Defaults to {@link defaultIsEmpty}. */ + /** Custom function to determine if content is empty. By default, only arrays with length 0 are considered empty. */ isEmpty?: (content: C) => boolean; } @@ -330,7 +330,7 @@ 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, uses {@link defaultIsEmpty} to check for empty arrays. + * By default, only arrays with length 0 are considered empty. */ export declare function NonEmptyContent( props: NonEmptyContentProps, @@ -342,7 +342,7 @@ export declare function NonEmptyContent( export declare interface NonEmptyContentProps { /** Content to render when the content is not empty. */ children?: ReactNode; - /** Custom function to determine if content is empty. Defaults to {@link defaultIsEmpty}. */ + /** Custom function to determine if content is empty. By default, only arrays with length 0 are considered empty. */ isEmpty?: (content: C) => boolean; } diff --git a/packages/ctablex-core/src/contents/empty-content.tsx b/packages/ctablex-core/src/contents/empty-content.tsx index fc77c59..ff68a78 100644 --- a/packages/ctablex-core/src/contents/empty-content.tsx +++ b/packages/ctablex-core/src/contents/empty-content.tsx @@ -7,7 +7,7 @@ import { useContent } from '../content-provider'; export interface EmptyContentProps { /** Content to render when the content is empty. */ children?: ReactNode; - /** Custom function to determine if content is empty. Defaults to {@link defaultIsEmpty}. */ + /** Custom function to determine if content is empty. By default, only arrays with length 0 are considered empty. */ isEmpty?: (content: C) => boolean; } @@ -24,7 +24,7 @@ export function defaultIsEmpty(content: C): boolean { * 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, uses {@link defaultIsEmpty} to check for empty arrays. + * By default, only arrays with length 0 are considered empty. */ export function EmptyContent(props: EmptyContentProps) { const { children, isEmpty = defaultIsEmpty } = props; diff --git a/packages/ctablex-core/src/contents/non-empty-content.tsx b/packages/ctablex-core/src/contents/non-empty-content.tsx index 8e16dea..9f90cb4 100644 --- a/packages/ctablex-core/src/contents/non-empty-content.tsx +++ b/packages/ctablex-core/src/contents/non-empty-content.tsx @@ -8,7 +8,7 @@ import { defaultIsEmpty } from './empty-content'; export interface NonEmptyContentProps { /** Content to render when the content is not empty. */ children?: ReactNode; - /** Custom function to determine if content is empty. Defaults to {@link defaultIsEmpty}. */ + /** Custom function to determine if content is empty. By default, only arrays with length 0 are considered empty. */ isEmpty?: (content: C) => boolean; } @@ -16,7 +16,7 @@ export interface NonEmptyContentProps { * 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, uses {@link defaultIsEmpty} to check for empty arrays. + * By default, only arrays with length 0 are considered empty. */ export function NonEmptyContent(props: NonEmptyContentProps) { const { children, isEmpty = defaultIsEmpty } = props;