Skip to content

Commit e42bc2e

Browse files
committed
Discriminate multiple property in LookupField configuration
1 parent 868fd55 commit e42bc2e

4 files changed

Lines changed: 97 additions & 45 deletions

File tree

homedocs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"preview": "astro preview",
1010
"astro": "astro",
1111
"prettify": "prettier --write .",
12-
"check-types": "tsc --noEmit"
12+
"check-types": "npx tsc --noEmit"
1313
},
1414
"dependencies": {
1515
"@astrojs/mdx": "^4.3.13",

homedocs/src/examples/forms/InfiniteLookupListExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const cityDb: City[] = Array.from({ length: 5000 }, (_, i) => ({
5151
// @model-end
5252

5353
// @controller
54-
class PageController extends Controller<typeof m> {
54+
class PageController extends Controller {
5555
onQueryPage(params: { query: string; pageSize: number; page: number }) {
5656
let { query, pageSize, page } = params;
5757
let regex = new RegExp(query, "gi");

homedocs/src/pages/docs/intro/breaking-changes.mdx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ versions of the framework.
1313

1414
## 26.2.0
1515

16-
### LookupField: `onQueryPage` replaces `onQuery` for infinite mode
16+
### LookupField: Improved TypeScript discrimination
17+
18+
The `LookupField` component now uses TypeScript discriminated unions based on the `infinite` and `multiple` props. This provides better type inference and catches invalid prop combinations at compile time.
19+
20+
#### `onQueryPage` replaces `onQuery` for infinite mode
1721

1822
When using `LookupField` with `infinite: true`, you must now use the `onQueryPage` prop instead of `onQuery`.
19-
This change improves TypeScript type inference by providing distinct callback signatures for standard and infinite modes.
2023

2124
**Before:**
2225
```tsx
@@ -44,6 +47,17 @@ The `onQuery` prop now only accepts a string query parameter for standard (non-i
4447

4548
**Backwards Compatibility:** The runtime includes a compatibility shim that copies `onQuery` to `onQueryPage` when `infinite: true` is set. This allows existing code to continue working, but TypeScript will report type errors. We recommend updating your code to use the new API.
4649

50+
#### `pageSize` is now exclusive to infinite mode
51+
52+
The `pageSize` prop is now only available when `infinite: true`. If you were using `pageSize` without `infinite`, remove it as it had no effect.
53+
54+
#### Props are now discriminated by `multiple`
55+
56+
- When `multiple: true`: use `records` and/or `values` props
57+
- When `multiple` is not set or `false`: use `value` and `text` props
58+
59+
TypeScript will now report errors if you mix props from different modes (e.g., using `value` with `multiple: true`).
60+
4761
## 26.1.0
4862

4963
### TypeScript Migration

packages/cx/src/widgets/form/LookupField.tsx

Lines changed: 79 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,7 @@ export interface LookupFieldPagedQueryParams {
6565
}
6666

6767
/** Common LookupField properties shared across all variants */
68-
interface LookupFieldBaseConfig<TOption = unknown, TRecord = unknown>
69-
extends FieldConfig {
70-
/** Defaults to `false`. Set to `true` to enable multiple selection. */
71-
multiple?: BooleanProp;
72-
73-
/** Selected value. Used only if `multiple` is set to `false`. */
74-
value?: Prop<number | string>;
75-
76-
/** A list of selected ids. Used only if `multiple` is set to `true`. */
77-
values?: Prop<(number | string)[]>;
78-
79-
/** A list of selected records. Used only if `multiple` is set to `true`. */
80-
records?: Prop<TRecord[]>;
81-
82-
/** Text associated with the selection. Used only if `multiple` is set to `false`. */
83-
text?: StringProp;
84-
68+
interface LookupFieldBaseConfig<TOption = unknown> extends FieldConfig {
8569
/** The opposite of `disabled`. */
8670
enabled?: BooleanProp;
8771

@@ -175,20 +159,12 @@ interface LookupFieldBaseConfig<TOption = unknown, TRecord = unknown>
175159
/** Allow dropdown enter key events to propagate for form submission. */
176160
submitOnDropdownEnterKey?: BooleanProp;
177161

178-
/** Number of items per page. Default is `100`. */
179-
pageSize?: number;
180-
181162
/** Allow quick selection of all displayed items on `Ctrl + A` key combination. */
182163
quickSelectAll?: boolean;
183164

184165
/** Parameters that affect filtering. */
185166
filterParams?: StructuredProp;
186167

187-
/** Used in multiple selection lookups to construct display text from multiple fields. */
188-
onGetRecordDisplayText?:
189-
| ((record: TRecord, instance: Instance) => string)
190-
| null;
191-
192168
/** Callback to create a filter function for given filter params. */
193169
onCreateVisibleOptionsFilter?:
194170
| string
@@ -210,42 +186,104 @@ interface LookupFieldBaseConfig<TOption = unknown, TRecord = unknown>
210186
) => unknown);
211187
}
212188

213-
/** LookupField with infinite scrolling - uses onQueryPage */
214-
interface LookupFieldInfiniteConfig<TOption = unknown, TRecord = unknown>
215-
extends LookupFieldBaseConfig<TOption, TRecord> {
189+
// =============================================================================
190+
// Composable interfaces for discriminated union
191+
// =============================================================================
192+
193+
/** Props for infinite mode: uses onQueryPage */
194+
interface LookupFieldInfiniteProps<TOption = unknown> {
216195
/** Enable infinite scrolling. */
217196
infinite: true;
218-
219-
/** Query function for infinite mode - receives paged query params. */
197+
/** Number of items per page. Default is `100`. */
198+
pageSize?: number;
199+
/** Query function for infinite mode. */
220200
onQueryPage:
221201
| string
222202
| ((
223203
params: LookupFieldPagedQueryParams,
224204
instance: Instance
225205
) => TOption[] | Promise<TOption[]>);
226-
227-
/** Not available in infinite mode. Use onQueryPage instead. */
206+
/** Not available in infinite mode. */
228207
onQuery?: never;
229208
}
230209

231-
/** LookupField standard mode - uses onQuery */
232-
interface LookupFieldStandardConfig<TOption = unknown, TRecord = unknown>
233-
extends LookupFieldBaseConfig<TOption, TRecord> {
210+
/** Props for standard mode: uses onQuery */
211+
interface LookupFieldStandardProps<TOption = unknown> {
234212
/** Standard mode (no infinite scrolling). */
235213
infinite?: false;
236-
237-
/** Query function for standard mode - receives string query. */
214+
/** Not available in standard mode. */
215+
pageSize?: never;
216+
/** Query function for standard mode. */
238217
onQuery?:
239218
| string
240219
| ((query: string, instance: Instance) => TOption[] | Promise<TOption[]>);
241-
242-
/** Not available in standard mode. Set infinite: true to use onQueryPage. */
220+
/** Not available in standard mode. */
243221
onQueryPage?: never;
244222
}
245223

224+
/** Props for multiple selection mode */
225+
interface LookupFieldMultipleProps<TRecord = unknown> {
226+
/** Enable multiple selection. */
227+
multiple: true;
228+
/** A list of selected ids. */
229+
values?: Prop<(number | string)[]>;
230+
/** A list of selected records. */
231+
records?: Prop<TRecord[]>;
232+
/** Custom display text for records. */
233+
onGetRecordDisplayText?:
234+
| ((record: TRecord, instance: Instance) => string)
235+
| null;
236+
/** Not available in multiple mode. */
237+
value?: never;
238+
/** Not available in multiple mode. */
239+
text?: never;
240+
}
241+
242+
/** Props for single selection mode */
243+
interface LookupFieldSingleProps {
244+
/** Single selection (default). */
245+
multiple?: false;
246+
/** Selected value. */
247+
value?: Prop<number | string>;
248+
/** Text associated with the selection. */
249+
text?: StringProp;
250+
/** Not available in single mode. */
251+
values?: never;
252+
/** Not available in single mode. */
253+
records?: never;
254+
/** Not available in single mode. */
255+
onGetRecordDisplayText?: never;
256+
}
257+
258+
// =============================================================================
259+
// 4 Discriminated Union Variants (2 infinite × 2 multiple)
260+
// =============================================================================
261+
262+
type LookupFieldInfiniteMultipleConfig<TOption = unknown, TRecord = unknown> =
263+
LookupFieldBaseConfig<TOption> &
264+
LookupFieldInfiniteProps<TOption> &
265+
LookupFieldMultipleProps<TRecord>;
266+
267+
type LookupFieldInfiniteSingleConfig<TOption = unknown> =
268+
LookupFieldBaseConfig<TOption> &
269+
LookupFieldInfiniteProps<TOption> &
270+
LookupFieldSingleProps;
271+
272+
type LookupFieldStandardMultipleConfig<TOption = unknown, TRecord = unknown> =
273+
LookupFieldBaseConfig<TOption> &
274+
LookupFieldStandardProps<TOption> &
275+
LookupFieldMultipleProps<TRecord>;
276+
277+
type LookupFieldStandardSingleConfig<TOption = unknown> =
278+
LookupFieldBaseConfig<TOption> &
279+
LookupFieldStandardProps<TOption> &
280+
LookupFieldSingleProps;
281+
246282
export type LookupFieldConfig<TOption = unknown, TRecord = unknown> =
247-
| LookupFieldInfiniteConfig<TOption, TRecord>
248-
| LookupFieldStandardConfig<TOption, TRecord>;
283+
| LookupFieldInfiniteMultipleConfig<TOption, TRecord>
284+
| LookupFieldInfiniteSingleConfig<TOption>
285+
| LookupFieldStandardMultipleConfig<TOption, TRecord>
286+
| LookupFieldStandardSingleConfig<TOption>;
249287

250288
export class LookupField<TOption = unknown, TRecord = unknown> extends Field<
251289
LookupFieldConfig<TOption, TRecord>

0 commit comments

Comments
 (0)