Skip to content

Commit 0f30182

Browse files
committed
Typed contexts
1 parent e097367 commit 0f30182

23 files changed

Lines changed: 200 additions & 70 deletions

MIGRATION.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,67 @@ export { Button, ButtonConfig } from "./Button";
404404
export { FlexBox, FlexBoxConfig } from "./FlexBox";
405405
```
406406

407+
## Typed RenderingContext Usage
408+
409+
RenderingContext uses `[key: string]: any` to allow dynamic property access. Parent widgets set context properties that children consume. To add type safety, define typed context interfaces that extend RenderingContext and use them directly in method signatures.
410+
411+
### Pattern: Typed Method Signatures
412+
413+
Define a typed context interface next to the widget that sets the context properties, then use it in method signatures:
414+
415+
```typescript
416+
// In ValidationGroup.ts
417+
import { RenderingContext } from "../../ui/RenderingContext";
418+
419+
/** Typed context interface for form-related context properties */
420+
export interface FormRenderingContext extends RenderingContext {
421+
parentDisabled?: boolean;
422+
parentReadOnly?: boolean;
423+
parentViewMode?: boolean | string;
424+
parentTabOnEnterKey?: boolean;
425+
parentVisited?: boolean;
426+
parentStrict?: boolean;
427+
parentAsterisk?: boolean;
428+
validation?: { errors: ValidationErrorData[] };
429+
lastFieldId?: string;
430+
}
431+
432+
export class ValidationGroup extends PureContainerBase<...> {
433+
// Use typed context directly in method signature
434+
explore(context: FormRenderingContext, instance: ValidationGroupInstance): void {
435+
context.push("parentStrict", coalesce(instance.data.strict, context.parentStrict));
436+
context.push("parentDisabled", coalesce(instance.data.disabled, context.parentDisabled));
437+
super.explore(context, instance);
438+
}
439+
}
440+
```
441+
442+
### Existing Typed Contexts
443+
444+
**FormRenderingContext** (ValidationGroup.ts):
445+
- Used by: ValidationGroup, Field, Label, ValidationError, Button
446+
- Properties: `parentDisabled`, `parentReadOnly`, `parentViewMode`, `parentTabOnEnterKey`, `parentVisited`, `parentStrict`, `parentAsterisk`, `validation`, `lastFieldId`
447+
448+
**SvgRenderingContext** (BoundedObject.ts):
449+
- Used by: Svg, BoundedObject and all SVG components
450+
- Properties: `parentRect`, `inSvg`, `addClipRect`
451+
452+
**ChartRenderingContext** (Chart.ts) - extends SvgRenderingContext:
453+
- Used by: Chart, all chart components (LineGraph, ScatterGraph, Gridlines, Marker, etc.)
454+
- Properties: `axes` (Record of axis calculators)
455+
456+
### Guidelines
457+
458+
1. **Define locally**: Keep context interfaces next to the widgets that define them
459+
2. **Export for consumers**: Export the interface so child widgets can import and use it
460+
3. **Extend appropriately**: ChartRenderingContext extends SvgRenderingContext since charts need `parentRect`
461+
4. **Use non-null assertion**: When accessing context properties that must exist, use `context.axes!`
462+
463+
---
464+
407465
## Nice to Have Improvements
408466

409-
[ ] Typed RenderingContext usage
467+
[x] Typed RenderingContext usage
410468
[ ] Better StructuredProp and typed ContentResolver
411469
[ ] dropdownOptions might typed as DropdownConfig?
412470
[x] Properly type Component.create, Widget.create, etc.

docs/content/intro/TypeScriptMigration.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,37 @@ export default (
132132
typed controller instance, enabling full autocomplete and compile-time type checking for
133133
controller methods and their parameters.
134134

135+
### Typed RenderingContext
136+
137+
CxJS uses a `RenderingContext` object to pass information down the widget tree during rendering.
138+
Different widget families define typed context interfaces that extend `RenderingContext` for
139+
type-safe access to context properties.
140+
141+
**Available typed contexts:**
142+
143+
- `FormRenderingContext` - Form validation context (`parentDisabled`, `parentReadOnly`, `validation`, etc.)
144+
- `SvgRenderingContext` - SVG layout context (`parentRect`, `inSvg`, `addClipRect`)
145+
- `ChartRenderingContext` - Chart context extending SVG (`axes`)
146+
147+
When creating custom widgets that consume these context properties, import and use the typed
148+
context interface in your method signatures:
149+
150+
<CodeSplit>
151+
<CodeSnippet copy={false}>{`
152+
import type { FormRenderingContext } from "cx/widgets";
153+
154+
export class MyFormWidget extends Field<MyFormWidgetConfig> {
155+
explore(context: FormRenderingContext, instance: Instance) {
156+
// Type-safe access to form context properties
157+
if (context.parentDisabled) {
158+
// handle disabled state
159+
}
160+
super.explore(context, instance);
161+
}
162+
}
163+
`}</CodeSnippet>
164+
</CodeSplit>
165+
135166
## Authoring Widgets
136167

137168
Previously, CxJS widgets had to be written in JavaScript with optional

packages/cx/src/charts/BubbleGraph.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Instance } from "../ui/Instance";
88
import { RenderingContext, CxChild } from "../ui/RenderingContext";
99
import { Prop, StyleProp, DataRecord } from "../ui/Prop";
1010
import { Create } from "../util/Component";
11+
import type { ChartRenderingContext } from "./Chart";
1112

1213
export interface BubbleGraphConfig extends WidgetConfig {
1314
/** Data array for the bubbles. */
@@ -80,8 +81,8 @@ export class BubbleGraph extends Widget<BubbleGraphConfig> {
8081
super.init();
8182
}
8283

83-
explore(context: RenderingContext, instance: BubbleGraphInstance) {
84-
instance.axes = context.axes;
84+
explore(context: ChartRenderingContext, instance: BubbleGraphInstance) {
85+
instance.axes = context.axes!;
8586
super.explore(context, instance);
8687
var { data } = instance;
8788
const d = data as any;
@@ -93,13 +94,13 @@ export class BubbleGraph extends Widget<BubbleGraphConfig> {
9394
}
9495
}
9596

96-
prepare(context: RenderingContext, instance: BubbleGraphInstance) {
97+
prepare(context: ChartRenderingContext, instance: BubbleGraphInstance) {
9798
super.prepare?.(context, instance);
9899
if (instance.axes[this.xAxis].shouldUpdate || (instance.axes as any)[this.yAxis].shouldUpdate)
99100
instance.markShouldUpdate(context);
100101
}
101102

102-
render(context: RenderingContext, instance: BubbleGraphInstance, key: string): CxChild {
103+
render(context: ChartRenderingContext, instance: BubbleGraphInstance, key: string): CxChild {
103104
var { data } = instance;
104105
return (
105106
<g key={key} className={(data as any).classNames}>
@@ -108,7 +109,7 @@ export class BubbleGraph extends Widget<BubbleGraphConfig> {
108109
);
109110
}
110111

111-
renderData(context: RenderingContext, instance: BubbleGraphInstance): any {
112+
renderData(context: ChartRenderingContext, instance: BubbleGraphInstance): any {
112113
var { data, axes, store } = instance;
113114
const d = data as any;
114115

packages/cx/src/charts/Chart.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { Widget, VDOM, getContent } from "../ui/Widget";
2-
import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject";
2+
import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance, SvgRenderingContext } from "../svg/BoundedObject";
33
import { Axis } from "./axis/Axis";
44
import { RenderingContext } from "../ui/RenderingContext";
55
import { Create } from "../util/Component";
66

7+
/** Typed context interface for chart-related context properties */
8+
export interface ChartRenderingContext extends SvgRenderingContext {
9+
axes?: Record<string, any>;
10+
}
11+
712
export interface ChartConfig extends BoundedObjectConfig {
813
/** Axis definition. Each key represent an axis, and each value hold axis configuration. */
914
axes?: Record<string, Create<typeof Axis>>;
@@ -35,7 +40,7 @@ export class Chart extends BoundedObject<ChartConfig, ChartInstance> {
3540
}
3641
}
3742

38-
explore(context: RenderingContext, instance: ChartInstance): void {
43+
explore(context: ChartRenderingContext, instance: ChartInstance): void {
3944
instance.calculators = { ...context.axes };
4045

4146
context.push("axes", instance.calculators);
@@ -53,25 +58,25 @@ export class Chart extends BoundedObject<ChartConfig, ChartInstance> {
5358
super.explore(context, instance);
5459
}
5560

56-
exploreCleanup(context: RenderingContext, instance: ChartInstance): void {
61+
exploreCleanup(context: ChartRenderingContext, instance: ChartInstance): void {
5762
context.pop("axes");
5863

5964
for (let axis in instance.axes) {
6065
instance.axes[axis].widget.reportData(context, instance.axes[axis]);
6166
}
6267
}
6368

64-
prepare(context: RenderingContext, instance: ChartInstance): void {
69+
prepare(context: ChartRenderingContext, instance: ChartInstance): void {
6570
context.push("axes", instance.calculators);
6671
super.prepare(context, instance);
6772
}
6873

69-
prepareCleanup(context: RenderingContext, instance: ChartInstance): void {
74+
prepareCleanup(context: ChartRenderingContext, instance: ChartInstance): void {
7075
context.pop("axes");
7176
super.prepareCleanup(context, instance);
7277
}
7378

74-
render(context: RenderingContext, instance: ChartInstance, key: string): any[] {
79+
render(context: ChartRenderingContext, instance: ChartInstance, key: string): any[] {
7580
let axes = [];
7681
for (let k in instance.axes) {
7782
axes.push(getContent(instance.axes[k].render(context, key + "-axis-" + k)));

packages/cx/src/charts/ColumnBarBase.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Prop, BooleanProp, NumberProp, StringProp } from "../ui/Prop";
99
import { Instance } from "../ui/Instance";
1010
import { RenderingContext } from "../ui/RenderingContext";
1111
import { Rect } from "../svg/util/Rect";
12+
import type { ChartRenderingContext } from "./Chart";
1213

1314
export interface ColumnBarBaseConfig extends StyledContainerConfig {
1415
/** The `x` value binding or expression. */
@@ -141,10 +142,10 @@ export class ColumnBarBase extends StyledContainerBase<ColumnBarBaseConfig> {
141142
);
142143
}
143144

144-
prepareData(context: RenderingContext, instance: ColumnBarBaseInstance): void {
145-
instance.axes = context.axes;
146-
instance.xAxis = context.axes[this.xAxis];
147-
instance.yAxis = context.axes[this.yAxis];
145+
prepareData(context: ChartRenderingContext, instance: ColumnBarBaseInstance): void {
146+
instance.axes = context.axes!;
147+
instance.xAxis = context.axes![this.xAxis];
148+
instance.yAxis = context.axes![this.yAxis];
148149
instance.hoverSync = context.hoverSync;
149150
var { data } = instance;
150151
data.valid = this.checkValid(data);
@@ -156,7 +157,7 @@ export class ColumnBarBase extends StyledContainerBase<ColumnBarBaseConfig> {
156157
return true;
157158
}
158159

159-
prepare(context: RenderingContext, instance: ColumnBarBaseInstance): void {
160+
prepare(context: ChartRenderingContext, instance: ColumnBarBaseInstance): void {
160161
let { data, colorMap } = instance;
161162

162163
if (colorMap && data.colorName) {
@@ -190,7 +191,7 @@ export class ColumnBarBase extends StyledContainerBase<ColumnBarBaseConfig> {
190191
});
191192
}
192193

193-
prepareCleanup(context: RenderingContext, instance: ColumnBarBaseInstance): void {
194+
prepareCleanup(context: ChartRenderingContext, instance: ColumnBarBaseInstance): void {
194195
let { data } = instance;
195196
if (data.valid && data.active) context.pop("parentRect");
196197
}

packages/cx/src/charts/ColumnBarGraphBase.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Selection } from "../ui/selection/Selection";
55
import { Instance } from "../ui/Instance";
66
import { RenderingContext } from "../ui/RenderingContext";
77
import { NumberProp, BooleanProp, StringProp, RecordsProp } from "../ui/Prop";
8+
import type { ChartRenderingContext } from "./Chart";
89

910
export interface ColumnBarGraphBaseConfig extends WidgetConfig {
1011
/**
@@ -148,9 +149,9 @@ export class ColumnBarGraphBase extends Widget<ColumnBarGraphBaseConfig> {
148149
super.prepareData(context, instance);
149150
}
150151

151-
explore(context: RenderingContext, instance: ColumnBarGraphBaseInstance): void {
152-
instance.xAxis = context.axes[this.xAxis];
153-
instance.yAxis = context.axes[this.yAxis];
152+
explore(context: ChartRenderingContext, instance: ColumnBarGraphBaseInstance): void {
153+
instance.xAxis = context.axes![this.xAxis];
154+
instance.yAxis = context.axes![this.yAxis];
154155

155156
var { data } = instance;
156157

@@ -160,7 +161,7 @@ export class ColumnBarGraphBase extends Widget<ColumnBarGraphBaseConfig> {
160161
super.explore(context, instance);
161162
}
162163

163-
prepare(context: RenderingContext, instance: ColumnBarGraphBaseInstance): void {
164+
prepare(context: ChartRenderingContext, instance: ColumnBarGraphBaseInstance): void {
164165
let { data, colorMap, xAxis, yAxis } = instance;
165166

166167
if (colorMap && data.name) {

packages/cx/src/charts/Gridlines.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject";
44
import { VDOM } from "../ui/Widget";
55
import { RenderingContext, CxChild } from "../ui/RenderingContext";
6+
import type { ChartRenderingContext } from "./Chart";
67

78
export interface GridlinesConfig extends BoundedObjectConfig {
89
/**
@@ -38,13 +39,13 @@ export class Gridlines extends BoundedObject<GridlinesConfig, GridlinesInstance>
3839
super(config);
3940
}
4041

41-
explore(context: RenderingContext, instance: GridlinesInstance) {
42+
explore(context: ChartRenderingContext, instance: GridlinesInstance) {
4243
super.explore(context, instance);
43-
instance.xAxis = (context.axes as any)?.[this.xAxis];
44-
instance.yAxis = (context.axes as any)?.[this.yAxis];
44+
instance.xAxis = context.axes?.[this.xAxis];
45+
instance.yAxis = context.axes?.[this.yAxis];
4546
}
4647

47-
prepare(context: RenderingContext, instance: GridlinesInstance) {
48+
prepare(context: ChartRenderingContext, instance: GridlinesInstance) {
4849
super.prepare(context, instance);
4950
let { xAxis, yAxis } = instance;
5051
if ((xAxis && xAxis.shouldUpdate) || (yAxis && yAxis.shouldUpdate)) instance.markShouldUpdate(context);

packages/cx/src/charts/LineGraph.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parseStyle } from "../util/parseStyle";
66
import { Instance } from "../ui/Instance";
77
import { RenderingContext } from "../ui/RenderingContext";
88
import { NumberProp, BooleanProp, StringProp, RecordsProp, StyleProp } from "../ui/Prop";
9+
import type { ChartRenderingContext } from "./Chart";
910

1011
interface LinePoint {
1112
x: number;
@@ -162,15 +163,15 @@ export class LineGraph extends Widget {
162163
super.prepareData(context, instance);
163164
}
164165

165-
explore(context: RenderingContext, instance: LineGraphInstance): void {
166+
explore(context: ChartRenderingContext, instance: LineGraphInstance): void {
166167
let { data } = instance;
167168

168169
instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap);
169170

170171
if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName);
171172

172173
if (data.active) {
173-
instance.axes = context.axes;
174+
instance.axes = context.axes!;
174175
instance.xAxis = instance.axes[this.xAxis];
175176
instance.yAxis = instance.axes[this.yAxis];
176177
super.explore(context, instance);
@@ -192,7 +193,7 @@ export class LineGraph extends Widget {
192193
}
193194
}
194195

195-
prepare(context: RenderingContext, instance: LineGraphInstance): void {
196+
prepare(context: ChartRenderingContext, instance: LineGraphInstance): void {
196197
let { data, colorMap } = instance;
197198

198199
if (colorMap && data.colorName) {

packages/cx/src/charts/Marker.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { getTopLevelBoundingClientRect } from "../util/getTopLevelBoundingClient
2121
import { RenderingContext } from "../ui/RenderingContext";
2222
import { NumberProp, BooleanProp, StringProp, StructuredProp } from "../ui/Prop";
2323
import { Instance } from "../ui/Instance";
24+
import type { ChartRenderingContext } from "./Chart";
2425

2526
export interface MarkerConfig extends BoundedObjectConfig {
2627
/** The `x` value binding or expression. */

packages/cx/src/charts/MarkerLine.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { isDefined } from "../util/isDefined";
66
import { Rect } from "../svg/util/Rect";
77
import { RenderingContext } from "../ui/RenderingContext";
88
import { NumberProp, BooleanProp, StringProp } from "../ui/Prop";
9+
import type { ChartRenderingContext } from "./Chart";
910

1011
export interface MarkerLineConfig extends BoundedObjectConfig {
1112
/** X coordinate for vertical line. */

0 commit comments

Comments
 (0)