@ic-reactor/candid 3.0.12-beta.0 → 3.0.13-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/metadata-display-reactor.d.ts.map +1 -1
- package/dist/metadata-display-reactor.js +2 -2
- package/dist/metadata-display-reactor.js.map +1 -1
- package/dist/visitor/arguments/helpers.d.ts +40 -0
- package/dist/visitor/arguments/helpers.d.ts.map +1 -0
- package/dist/visitor/arguments/helpers.js +81 -0
- package/dist/visitor/arguments/helpers.js.map +1 -0
- package/dist/visitor/arguments/index.d.ts +8 -5
- package/dist/visitor/arguments/index.d.ts.map +1 -1
- package/dist/visitor/arguments/index.js +212 -17
- package/dist/visitor/arguments/index.js.map +1 -1
- package/dist/visitor/arguments/types.d.ts +282 -30
- package/dist/visitor/arguments/types.d.ts.map +1 -1
- package/dist/visitor/arguments/types.js +1 -40
- package/dist/visitor/arguments/types.js.map +1 -1
- package/package.json +2 -2
- package/src/metadata-display-reactor.ts +2 -2
- package/src/visitor/arguments/helpers.ts +104 -0
- package/src/visitor/arguments/index.test.ts +401 -2
- package/src/visitor/arguments/index.ts +251 -17
- package/src/visitor/arguments/schema.test.ts +3 -3
- package/src/visitor/arguments/types.ts +318 -52
- package/src/visitor/arguments/README.md +0 -230
|
@@ -21,20 +21,119 @@ export type ArgumentFieldType =
|
|
|
21
21
|
| "unknown"
|
|
22
22
|
|
|
23
23
|
// ════════════════════════════════════════════════════════════════════════════
|
|
24
|
-
//
|
|
24
|
+
// Component Type Hints
|
|
25
25
|
// ════════════════════════════════════════════════════════════════════════════
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Suggested component type for rendering the field.
|
|
29
|
+
* This eliminates the need for switch statements in the frontend.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const componentMap = {
|
|
34
|
+
* 'text-input': TextField,
|
|
35
|
+
* 'number-input': NumberField,
|
|
36
|
+
* 'boolean-checkbox': BooleanField,
|
|
37
|
+
* // ...
|
|
38
|
+
* }
|
|
39
|
+
* const Component = componentMap[field.component]
|
|
40
|
+
* return <Component field={field} />
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export type FieldComponentType =
|
|
44
|
+
| "record-container"
|
|
45
|
+
| "tuple-container"
|
|
46
|
+
| "variant-select"
|
|
47
|
+
| "optional-toggle"
|
|
48
|
+
| "vector-list"
|
|
49
|
+
| "blob-upload"
|
|
50
|
+
| "principal-input"
|
|
51
|
+
| "text-input"
|
|
52
|
+
| "number-input"
|
|
53
|
+
| "boolean-checkbox"
|
|
54
|
+
| "null-hidden"
|
|
55
|
+
| "recursive-lazy"
|
|
56
|
+
| "unknown-fallback"
|
|
57
|
+
|
|
58
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
59
|
+
// Render Hints for UI Rendering Strategy
|
|
60
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Input type hints for HTML input elements.
|
|
64
|
+
* Used by primitive fields to suggest the appropriate input type.
|
|
65
|
+
*/
|
|
66
|
+
export type InputType =
|
|
67
|
+
| "text"
|
|
68
|
+
| "number"
|
|
69
|
+
| "checkbox"
|
|
70
|
+
| "select"
|
|
71
|
+
| "file"
|
|
72
|
+
| "textarea"
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Rendering hints for the UI.
|
|
76
|
+
* Eliminates the need for frontend to maintain COMPLEX_TYPES arrays.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* // Frontend no longer needs:
|
|
81
|
+
* // const COMPLEX_TYPES = ["record", "tuple", "variant", "vector", "optional"]
|
|
82
|
+
*
|
|
83
|
+
* // Instead use:
|
|
84
|
+
* if (field.renderHint.isCompound) {
|
|
85
|
+
* return <CompoundFieldRenderer field={field} />
|
|
86
|
+
* }
|
|
87
|
+
* return <PrimitiveInput field={field} />
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export interface RenderHint {
|
|
91
|
+
/** Whether this field has its own container/card styling (compound types) */
|
|
92
|
+
isCompound: boolean
|
|
93
|
+
/** Whether this is a leaf input (primitive types) */
|
|
94
|
+
isPrimitive: boolean
|
|
95
|
+
/** Suggested input type for HTML input elements */
|
|
96
|
+
inputType?: InputType
|
|
97
|
+
/** Description or help text for the field (derived from Candid) */
|
|
31
98
|
description?: string
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
102
|
+
// Primitive Input Props
|
|
103
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pre-computed HTML input props for primitive fields.
|
|
107
|
+
* Can be spread directly onto an input element.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```tsx
|
|
111
|
+
* <input {...field.inputProps} value={value} onChange={handleChange} />
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export interface PrimitiveInputProps {
|
|
115
|
+
/** HTML input type */
|
|
116
|
+
type?: "text" | "number" | "checkbox"
|
|
117
|
+
/** Placeholder text */
|
|
118
|
+
placeholder?: string
|
|
119
|
+
/** Minimum value for number inputs */
|
|
120
|
+
min?: string | number
|
|
121
|
+
/** Maximum value for number inputs */
|
|
122
|
+
max?: string | number
|
|
123
|
+
/** Step value for number inputs */
|
|
124
|
+
step?: string | number
|
|
125
|
+
/** Pattern for text inputs */
|
|
126
|
+
pattern?: string
|
|
127
|
+
/** Input mode for virtual keyboards */
|
|
128
|
+
inputMode?: "text" | "numeric" | "decimal"
|
|
129
|
+
/** Autocomplete hint */
|
|
130
|
+
autoComplete?: string
|
|
131
|
+
/** Whether to check spelling */
|
|
132
|
+
spellCheck?: boolean
|
|
133
|
+
/** Minimum length for text inputs */
|
|
134
|
+
minLength?: number
|
|
135
|
+
/** Maximum length for text inputs */
|
|
136
|
+
maxLength?: number
|
|
38
137
|
}
|
|
39
138
|
|
|
40
139
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -44,8 +143,18 @@ export interface FieldUIHints {
|
|
|
44
143
|
export interface FieldBase<TValue = unknown> {
|
|
45
144
|
/** The field type */
|
|
46
145
|
type: ArgumentFieldType
|
|
47
|
-
/**
|
|
146
|
+
/** Raw label from Candid: "__arg0", "_0_" */
|
|
48
147
|
label: string
|
|
148
|
+
/**
|
|
149
|
+
* Pre-formatted display label for UI rendering.
|
|
150
|
+
* Transforms raw labels into human-readable format.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* "__arg0" => "Arg 0"
|
|
154
|
+
* "_0_" => "Item 0"
|
|
155
|
+
* "created_at_time" => "Created At Time"
|
|
156
|
+
*/
|
|
157
|
+
displayLabel: string
|
|
49
158
|
/**
|
|
50
159
|
* Form field name path for binding.
|
|
51
160
|
* Uses bracket notation for array indices: `[0]`, `args[0].owner`, `tags[1]`
|
|
@@ -59,14 +168,22 @@ export interface FieldBase<TValue = unknown> {
|
|
|
59
168
|
* ```
|
|
60
169
|
*/
|
|
61
170
|
name: string
|
|
171
|
+
/**
|
|
172
|
+
* Suggested component type for rendering this field.
|
|
173
|
+
* Eliminates the need for switch statements in the frontend.
|
|
174
|
+
*/
|
|
175
|
+
component: FieldComponentType
|
|
176
|
+
/**
|
|
177
|
+
* Rendering hints for UI strategy.
|
|
178
|
+
* Use this to determine if the field needs a container or is a simple input.
|
|
179
|
+
*/
|
|
180
|
+
renderHint: RenderHint
|
|
62
181
|
/** Zod schema for field validation */
|
|
63
182
|
schema: z.ZodTypeAny
|
|
64
183
|
/** Default value for the field */
|
|
65
184
|
defaultValue: TValue
|
|
66
185
|
/** Original Candid type name for reference */
|
|
67
186
|
candidType?: string
|
|
68
|
-
/** UI rendering hints */
|
|
69
|
-
ui?: FieldUIHints
|
|
70
187
|
}
|
|
71
188
|
|
|
72
189
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -104,6 +221,43 @@ export interface VariantField extends FieldBase<Record<string, unknown>> {
|
|
|
104
221
|
* ```
|
|
105
222
|
*/
|
|
106
223
|
getOptionDefault: (option: string) => Record<string, unknown>
|
|
224
|
+
/**
|
|
225
|
+
* Get the field for a specific option.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```tsx
|
|
229
|
+
* const transferField = field.getField("Transfer")
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
getField: (option: string) => Field
|
|
233
|
+
/**
|
|
234
|
+
* Get the currently selected option from a value.
|
|
235
|
+
* Returns the first valid key found, or the default option.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```tsx
|
|
239
|
+
* const selectedOption = field.getSelectedOption(currentValue)
|
|
240
|
+
* // { Transfer: {...} } => "Transfer"
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
getSelectedOption: (value: Record<string, unknown>) => string
|
|
244
|
+
/**
|
|
245
|
+
* Get the selected field from a value.
|
|
246
|
+
* Combines getSelectedOption and getField for convenience.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```tsx
|
|
250
|
+
* // Current (verbose):
|
|
251
|
+
* const validKeys = Object.keys(currentValue).filter(k => field.options.includes(k))
|
|
252
|
+
* const selected = validKeys[0] ?? field.options[0]
|
|
253
|
+
* const selectedIndex = Math.max(0, field.options.indexOf(selected))
|
|
254
|
+
* const selectedField = field.fields[selectedIndex]
|
|
255
|
+
*
|
|
256
|
+
* // Proposed (simple):
|
|
257
|
+
* const selectedField = field.getSelectedField(currentValue)
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
getSelectedField: (value: Record<string, unknown>) => Field
|
|
107
261
|
}
|
|
108
262
|
|
|
109
263
|
export interface TupleField extends FieldBase<unknown[]> {
|
|
@@ -132,6 +286,20 @@ export interface OptionalField extends FieldBase<null> {
|
|
|
132
286
|
* ```
|
|
133
287
|
*/
|
|
134
288
|
getInnerDefault: () => unknown
|
|
289
|
+
/**
|
|
290
|
+
* Check if a value represents an enabled optional.
|
|
291
|
+
* Returns true if the value is not null or undefined.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```tsx
|
|
295
|
+
* // Current:
|
|
296
|
+
* const enabled = fieldApi.state.value !== null && typeof fieldApi.state.value !== "undefined"
|
|
297
|
+
*
|
|
298
|
+
* // Proposed:
|
|
299
|
+
* const enabled = field.isEnabled(fieldApi.state.value)
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
isEnabled: (value: unknown) => boolean
|
|
135
303
|
}
|
|
136
304
|
|
|
137
305
|
export interface VectorField extends FieldBase<unknown[]> {
|
|
@@ -150,6 +318,47 @@ export interface VectorField extends FieldBase<unknown[]> {
|
|
|
150
318
|
* ```
|
|
151
319
|
*/
|
|
152
320
|
getItemDefault: () => unknown
|
|
321
|
+
/**
|
|
322
|
+
* Create a properly configured item field for a specific index.
|
|
323
|
+
* Handles name path and label generation.
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```tsx
|
|
327
|
+
* // Current:
|
|
328
|
+
* renderField({
|
|
329
|
+
* ...field.itemField,
|
|
330
|
+
* label: itemLabel,
|
|
331
|
+
* name: itemFieldName
|
|
332
|
+
* })
|
|
333
|
+
*
|
|
334
|
+
* // Proposed:
|
|
335
|
+
* const itemField = field.createItemField(index, { label: itemLabel })
|
|
336
|
+
* renderField(itemField)
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
createItemField: (index: number, overrides?: { label?: string }) => Field
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Blob field size limits.
|
|
344
|
+
*/
|
|
345
|
+
export interface BlobLimits {
|
|
346
|
+
/** Maximum bytes when entering as hex (e.g., 512 bytes) */
|
|
347
|
+
maxHexBytes: number
|
|
348
|
+
/** Maximum file size in bytes (e.g., 2MB ICP limit) */
|
|
349
|
+
maxFileBytes: number
|
|
350
|
+
/** Maximum hex display length before truncation */
|
|
351
|
+
maxHexDisplayLength: number
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Validation result for blob input.
|
|
356
|
+
*/
|
|
357
|
+
export interface BlobValidationResult {
|
|
358
|
+
/** Whether the input is valid */
|
|
359
|
+
valid: boolean
|
|
360
|
+
/** Error message if invalid */
|
|
361
|
+
error?: string
|
|
153
362
|
}
|
|
154
363
|
|
|
155
364
|
export interface BlobField extends FieldBase<string> {
|
|
@@ -158,6 +367,30 @@ export interface BlobField extends FieldBase<string> {
|
|
|
158
367
|
itemField: Field
|
|
159
368
|
/** Accepted input formats */
|
|
160
369
|
acceptedFormats: ("hex" | "base64" | "file")[]
|
|
370
|
+
/** Size limits for blob input */
|
|
371
|
+
limits: BlobLimits
|
|
372
|
+
/**
|
|
373
|
+
* Normalize hex input (remove 0x prefix, lowercase, etc.)
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```tsx
|
|
377
|
+
* const normalized = field.normalizeHex("0xDEADBEEF")
|
|
378
|
+
* // => "deadbeef"
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
normalizeHex: (input: string) => string
|
|
382
|
+
/**
|
|
383
|
+
* Validate blob input value.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```tsx
|
|
387
|
+
* const result = field.validateInput(value)
|
|
388
|
+
* if (!result.valid) {
|
|
389
|
+
* setError(result.error)
|
|
390
|
+
* }
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
validateInput: (value: string | Uint8Array) => BlobValidationResult
|
|
161
394
|
}
|
|
162
395
|
|
|
163
396
|
export interface RecursiveField extends FieldBase<undefined> {
|
|
@@ -181,6 +414,14 @@ export interface PrincipalField extends FieldBase<string> {
|
|
|
181
414
|
type: "principal"
|
|
182
415
|
maxLength: number
|
|
183
416
|
minLength: number
|
|
417
|
+
/**
|
|
418
|
+
* Pre-computed HTML input props for direct spreading.
|
|
419
|
+
* @example
|
|
420
|
+
* ```tsx
|
|
421
|
+
* <input {...field.inputProps} value={value} onChange={handleChange} />
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
inputProps: PrimitiveInputProps
|
|
184
425
|
}
|
|
185
426
|
|
|
186
427
|
export interface NumberField extends FieldBase<string> {
|
|
@@ -199,6 +440,14 @@ export interface NumberField extends FieldBase<string> {
|
|
|
199
440
|
min?: string
|
|
200
441
|
/** Maximum value constraint (for bounded types) */
|
|
201
442
|
max?: string
|
|
443
|
+
/**
|
|
444
|
+
* Pre-computed HTML input props for direct spreading.
|
|
445
|
+
* @example
|
|
446
|
+
* ```tsx
|
|
447
|
+
* <input {...field.inputProps} value={value} onChange={handleChange} />
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
inputProps: PrimitiveInputProps
|
|
202
451
|
}
|
|
203
452
|
|
|
204
453
|
export interface TextField extends FieldBase<string> {
|
|
@@ -209,10 +458,26 @@ export interface TextField extends FieldBase<string> {
|
|
|
209
458
|
maxLength?: number
|
|
210
459
|
/** Whether to render as multiline textarea */
|
|
211
460
|
multiline?: boolean
|
|
461
|
+
/**
|
|
462
|
+
* Pre-computed HTML input props for direct spreading.
|
|
463
|
+
* @example
|
|
464
|
+
* ```tsx
|
|
465
|
+
* <input {...field.inputProps} value={value} onChange={handleChange} />
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
inputProps: PrimitiveInputProps
|
|
212
469
|
}
|
|
213
470
|
|
|
214
471
|
export interface BooleanField extends FieldBase<boolean> {
|
|
215
472
|
type: "boolean"
|
|
473
|
+
/**
|
|
474
|
+
* Pre-computed HTML input props for direct spreading.
|
|
475
|
+
* @example
|
|
476
|
+
* ```tsx
|
|
477
|
+
* <input {...field.inputProps} checked={value} onChange={handleChange} />
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
inputProps: PrimitiveInputProps
|
|
216
481
|
}
|
|
217
482
|
|
|
218
483
|
export interface NullField extends FieldBase<null> {
|
|
@@ -327,6 +592,28 @@ export type FieldByType<T extends ArgumentFieldType> = Extract<
|
|
|
327
592
|
{ type: T }
|
|
328
593
|
>
|
|
329
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Props type helper for field components.
|
|
597
|
+
* Use this to type your field components for better DX.
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* ```tsx
|
|
601
|
+
* const VariantField: React.FC<FieldProps<'variant'>> = ({ field, renderField }) => {
|
|
602
|
+
* // field is properly typed as VariantField
|
|
603
|
+
* return (
|
|
604
|
+
* <div>
|
|
605
|
+
* <select>{field.options.map(opt => ...)}</select>
|
|
606
|
+
* {renderField?.(field.getSelectedField(currentValue))}
|
|
607
|
+
* </div>
|
|
608
|
+
* )
|
|
609
|
+
* }
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
612
|
+
export type FieldProps<T extends ArgumentFieldType> = {
|
|
613
|
+
field: FieldByType<T>
|
|
614
|
+
renderField?: (child: Field) => React.ReactNode
|
|
615
|
+
}
|
|
616
|
+
|
|
330
617
|
/** Compound field types that contain other fields */
|
|
331
618
|
export type CompoundField =
|
|
332
619
|
| RecordField
|
|
@@ -345,50 +632,29 @@ export type PrimitiveField =
|
|
|
345
632
|
| NullField
|
|
346
633
|
|
|
347
634
|
/**
|
|
348
|
-
*
|
|
635
|
+
* A complete mapping of component types to React components.
|
|
636
|
+
* Use this type when defining your component map.
|
|
349
637
|
*
|
|
350
638
|
* @example
|
|
351
639
|
* ```tsx
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* return <RecordInput field={field} />
|
|
356
|
-
* }
|
|
357
|
-
* if (isFieldType(field, 'text')) {
|
|
358
|
-
* // field is now typed as TextField
|
|
359
|
-
* return <TextInput field={field} />
|
|
360
|
-
* }
|
|
640
|
+
* const componentMap: ComponentMap<typeof MyTextInput, typeof MyNumberInput, ...> = {
|
|
641
|
+
* 'text-input': MyTextInput,
|
|
642
|
+
* 'number-input': MyNumberInput,
|
|
361
643
|
* // ...
|
|
362
644
|
* }
|
|
363
645
|
* ```
|
|
364
646
|
*/
|
|
365
|
-
export
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return field.type === type
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/** Check if a field is a compound type (contains other fields) */
|
|
373
|
-
export function isCompoundField(field: Field): field is CompoundField {
|
|
374
|
-
return [
|
|
375
|
-
"record",
|
|
376
|
-
"variant",
|
|
377
|
-
"tuple",
|
|
378
|
-
"optional",
|
|
379
|
-
"vector",
|
|
380
|
-
"recursive",
|
|
381
|
-
].includes(field.type)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** Check if a field is a primitive type */
|
|
385
|
-
export function isPrimitiveField(field: Field): field is PrimitiveField {
|
|
386
|
-
return ["principal", "number", "text", "boolean", "null"].includes(field.type)
|
|
647
|
+
export type ComponentMap<
|
|
648
|
+
TComponents extends Record<FieldComponentType, unknown>,
|
|
649
|
+
> = {
|
|
650
|
+
[K in FieldComponentType]: TComponents[K]
|
|
387
651
|
}
|
|
388
652
|
|
|
389
|
-
/**
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
653
|
+
/**
|
|
654
|
+
* Get the component type for a given field component type.
|
|
655
|
+
* Useful for typing dynamic component lookups.
|
|
656
|
+
*/
|
|
657
|
+
export type GetComponentType<
|
|
658
|
+
TMap extends Partial<Record<FieldComponentType, unknown>>,
|
|
659
|
+
TKey extends FieldComponentType,
|
|
660
|
+
> = TKey extends keyof TMap ? TMap[TKey] : never
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
# Argument Field Visitor
|
|
2
|
-
|
|
3
|
-
The `ArgumentFieldVisitor` traverses Candid IDL types to generate two things:
|
|
4
|
-
|
|
5
|
-
1. **Field Metadata**: Structure, labels, names (for form binding), and default values for rendering form fields.
|
|
6
|
-
2. **Validation Schema**: A Zod schema for validating form inputs.
|
|
7
|
-
|
|
8
|
-
## Usage
|
|
9
|
-
|
|
10
|
-
### 1. Initialize the Visitor
|
|
11
|
-
|
|
12
|
-
```typescript
|
|
13
|
-
import { ArgumentFieldVisitor } from "@ic-reactor/candid"
|
|
14
|
-
import { IDL } from "@icp-sdk/core/candid"
|
|
15
|
-
|
|
16
|
-
const visitor = new ArgumentFieldVisitor()
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
### 2. Generate Metadata & Schema
|
|
20
|
-
|
|
21
|
-
You can visit a single function or an entire service.
|
|
22
|
-
|
|
23
|
-
#### For a Service
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
const serviceMeta = visitor.visitService(idlFactory({ IDL }))
|
|
27
|
-
const transferMeta = serviceMeta["icrc1_transfer"]
|
|
28
|
-
|
|
29
|
-
console.log(transferMeta)
|
|
30
|
-
// Output:
|
|
31
|
-
// {
|
|
32
|
-
// functionName: "icrc1_transfer",
|
|
33
|
-
// functionType: "update",
|
|
34
|
-
// fields: [...], // Field definitions for rendering
|
|
35
|
-
// defaultValues: [...], // Default values for the form (array of argument defaults)
|
|
36
|
-
// schema: ZodSchema, // Zod schema for validation
|
|
37
|
-
// argCount: 1, // Number of arguments
|
|
38
|
-
// isNoArgs: false // Whether the function takes no arguments
|
|
39
|
-
// }
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
#### For a Single Function
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
const funcType = IDL.Func([IDL.Text, IDL.Nat], [], [])
|
|
46
|
-
const meta = visitor.visitFunc(funcType, "myMethod")
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### 3. Field Properties
|
|
50
|
-
|
|
51
|
-
Each field in `meta.fields` has the following properties:
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
{
|
|
55
|
-
type: "text" | "number" | "boolean" | "principal" | "record" | "variant" | ...,
|
|
56
|
-
label: "fieldName", // Human-readable label
|
|
57
|
-
name: "[0].field.nested", // TanStack Form compatible path
|
|
58
|
-
defaultValue: ..., // Default value for this field
|
|
59
|
-
schema: ZodSchema, // Zod schema for this field
|
|
60
|
-
candidType: "text", // Original Candid type
|
|
61
|
-
ui: { // Optional UI hints
|
|
62
|
-
placeholder: "e.g. 100",
|
|
63
|
-
},
|
|
64
|
-
// Type-specific properties:
|
|
65
|
-
// - For "number" fields (Nat8, Int32, Float): min, max, unsigned, isFloat, bits
|
|
66
|
-
// - For "text" fields (Nat, Int, Nat64): (handled as text for BigInt support)
|
|
67
|
-
// - For variants: options, optionMap, getOptionDefault()
|
|
68
|
-
// - For vectors: itemField, getItemDefault()
|
|
69
|
-
// - For optionals: innerField, getInnerDefault()
|
|
70
|
-
// - For records: fields, fieldMap
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### 4. Special Handling & Validation
|
|
75
|
-
|
|
76
|
-
#### BigInts as Text
|
|
77
|
-
|
|
78
|
-
Large integer types (`Nat`, `Int`, `Nat64`, `Int64`, `Nat32` > 32-bit representations) are generated with `type: "text"`.
|
|
79
|
-
|
|
80
|
-
- **Reason**: Standard JavaScript numbers lose precision for values > `2^53 - 1`. HTML number inputs can be unreliable for large integers.
|
|
81
|
-
- **Validation**: The Zod schema strictly validates these as **strings containing only digits** (or sign for signed types).
|
|
82
|
-
- **Label**: They retain their `candidType` (e.g. `nat`) for reference.
|
|
83
|
-
|
|
84
|
-
#### Strict Validation
|
|
85
|
-
|
|
86
|
-
- **Required Fields**: Text and Number fields include `.min(1, "Required")`. Empty strings are rejected.
|
|
87
|
-
- **Integers**: Regex validation ensures only digits (no decimals).
|
|
88
|
-
- **Floats**: Float32/Float64 allow decimal points (e.g., `123.1`) and are validated using standard `!isNaN(Number(val))`.
|
|
89
|
-
- **Principals**: Validated using `Principal.fromText()`. Empty strings are rejected.
|
|
90
|
-
|
|
91
|
-
#### Optional Fields
|
|
92
|
-
|
|
93
|
-
- **Behavior**: Optional fields (`Opt`) wrap the inner schema.
|
|
94
|
-
- **Empty Handling**: An empty string input (`""`) is automatically transformed to `null` (Candid `null` / `None`), ensuring optional fields can be cleared.
|
|
95
|
-
|
|
96
|
-
### 5. Integration with TanStack Form
|
|
97
|
-
|
|
98
|
-
The visitor is optimized for standard form libraries like TanStack Form.
|
|
99
|
-
|
|
100
|
-
```tsx
|
|
101
|
-
import { useForm } from "@tanstack/react-form"
|
|
102
|
-
|
|
103
|
-
function MethodForm({ meta }) {
|
|
104
|
-
const form = useForm({
|
|
105
|
-
defaultValues: meta.defaultValues,
|
|
106
|
-
validators: {
|
|
107
|
-
onChange: meta.schema, // Use generated Zod schema for validation
|
|
108
|
-
},
|
|
109
|
-
onSubmit: async ({ value }) => {
|
|
110
|
-
console.log("Structured Data:", value)
|
|
111
|
-
// value is ready to be passed to strict Candid adapters
|
|
112
|
-
},
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<form
|
|
117
|
-
onSubmit={(e) => {
|
|
118
|
-
e.preventDefault()
|
|
119
|
-
e.stopPropagation()
|
|
120
|
-
form.handleSubmit()
|
|
121
|
-
}}
|
|
122
|
-
>
|
|
123
|
-
{meta.fields.map((field) => (
|
|
124
|
-
<form.Field key={field.name} name={field.name}>
|
|
125
|
-
{(fieldApi) => (
|
|
126
|
-
<div>
|
|
127
|
-
<label>{field.label}</label>
|
|
128
|
-
<input
|
|
129
|
-
type={
|
|
130
|
-
field.type === "text" || field.type === "principal"
|
|
131
|
-
? "text"
|
|
132
|
-
: field.type
|
|
133
|
-
}
|
|
134
|
-
value={fieldApi.state.value}
|
|
135
|
-
onChange={(e) => fieldApi.handleChange(e.target.value)}
|
|
136
|
-
placeholder={field.ui?.placeholder}
|
|
137
|
-
/>
|
|
138
|
-
{fieldApi.state.meta.errors.map((err) => (
|
|
139
|
-
<span key={err} className="error">
|
|
140
|
-
{err}
|
|
141
|
-
</span>
|
|
142
|
-
))}
|
|
143
|
-
</div>
|
|
144
|
-
)}
|
|
145
|
-
</form.Field>
|
|
146
|
-
))}
|
|
147
|
-
<button type="submit">Submit</button>
|
|
148
|
-
</form>
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### 6. Dynamic Fields
|
|
154
|
-
|
|
155
|
-
For **Vectors** and **Variants**, you can access helper paths dynamically:
|
|
156
|
-
|
|
157
|
-
- **Vector**: Field name `items` -> Item name `items[0]`, `items[1]`.
|
|
158
|
-
- **Record**: Field name `user` -> Nested `user.name`.
|
|
159
|
-
|
|
160
|
-
The `name` property in the metadata is pre-calculated to match this structure (e.g., `[0].args.user.name` if it's the first argument).
|
|
161
|
-
|
|
162
|
-
### 7. Working with Vectors (Arrays)
|
|
163
|
-
|
|
164
|
-
Use helper methods like `getItemDefault()` to manage array items.
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
function VectorField({ field, form }) {
|
|
168
|
-
return (
|
|
169
|
-
<form.Field name={field.name} mode="array">
|
|
170
|
-
{(arrayFieldApi) => (
|
|
171
|
-
<div>
|
|
172
|
-
{arrayFieldApi.state.value.map((_, index) => (
|
|
173
|
-
/* Render items using field.name + [index] */
|
|
174
|
-
))}
|
|
175
|
-
<button
|
|
176
|
-
onClick={() => arrayFieldApi.pushValue(field.getItemDefault())}
|
|
177
|
-
>
|
|
178
|
-
Add Item
|
|
179
|
-
</button>
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
182
|
-
</form.Field>
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### 8. Working with Variants
|
|
188
|
-
|
|
189
|
-
Use `optionMap` for lookup and `getOptionDefault()` for switching types.
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
<select
|
|
193
|
-
onChange={(e) => {
|
|
194
|
-
// Switch variant type and default value
|
|
195
|
-
const newValue = field.getOptionDefault(e.target.value)
|
|
196
|
-
form.setFieldValue(field.name, newValue)
|
|
197
|
-
}}
|
|
198
|
-
>
|
|
199
|
-
{field.options.map((opt) => (
|
|
200
|
-
<option key={opt}>{opt}</option>
|
|
201
|
-
))}
|
|
202
|
-
</select>
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### 9. Type Guards
|
|
206
|
-
|
|
207
|
-
The library exports type guard utilities for safer type narrowing:
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
import {
|
|
211
|
-
isFieldType,
|
|
212
|
-
isCompoundField,
|
|
213
|
-
isPrimitiveField,
|
|
214
|
-
} from "@ic-reactor/candid"
|
|
215
|
-
|
|
216
|
-
if (isFieldType(field, "record")) {
|
|
217
|
-
// field is RecordArgumentField
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### 10. Recursive Types
|
|
222
|
-
|
|
223
|
-
Recursive types (like linked lists) use `z.lazy()` schemas. Use `field.extract()` to get the inner definition when rendering.
|
|
224
|
-
|
|
225
|
-
```tsx
|
|
226
|
-
function RecursiveField({ field }) {
|
|
227
|
-
const innerField = useMemo(() => field.extract(), [field])
|
|
228
|
-
return <DynamicField field={innerField} ... />
|
|
229
|
-
}
|
|
230
|
-
```
|