Skip to content
IC Reactor

CandidFormVisitor

CandidFormVisitor generates form metadata from Candid IDL types.

Use it when you already have an IDL.ServiceClass and want low-level metadata for dynamic forms. If you want a higher-level API, use MetadataReactor which uses this visitor internally.

import { CandidFormVisitor } from "@ic-reactor/candid"
import { CandidFormVisitor } from "@ic-reactor/candid"
import { IDL } from "@icp-sdk/core/candid"
const visitor = new CandidFormVisitor()
const service = idlFactory({ IDL })
const serviceMeta = visitor.visitService(service)
const methodMeta = serviceMeta["icrc1_transfer"]
console.log(methodMeta?.schema) // Zod tuple schema for all args
console.log(methodMeta?.defaults) // default form values
type FormArgumentsMeta = {
candidType: string
functionType: "query" | "update" | "value"
functionName: string
args: FormFieldNode[]
defaults: unknown[]
schema: z.ZodTypeAny
argCount: number
isEmpty: boolean
}

Every FormFieldNode includes:

Property Type Description
type FormFieldType Candid field category
label string Raw field label
displayLabel string Human-readable label
name string Form path
schema z.ZodTypeAny Field-level validation schema
component FormFieldComponentType Suggested renderer component
renderHint FormRenderHint Primitive/compound and input type hints
defaultValue unknown Default value for initialization
candidType string Original Candid type text

Typical component values:

  • record-container
  • tuple-container
  • variant-select
  • optional-toggle
  • vector-list
  • blob-upload
  • principal-input
  • text-input
  • number-input
  • boolean-checkbox
  • null-hidden
  • recursive-lazy
  • unknown-fallback

renderHint shape:

type FormRenderHint = {
isCompound: boolean
isPrimitive: boolean
inputType?: "text" | "number" | "checkbox" | "select" | "file" | "textarea"
}

The form metadata holds display types: text for principals and numbers, null for an empty opt, { _type } for a variant. An actor takes Candid values, so the submitted values are converted to Candid before the call.

import { didToDisplayCodec } from "@ic-reactor/core"
import { IDL } from "@icp-sdk/core/candid"
import { useForm } from "@tanstack/react-form"
const { argTypes } = service.fieldsAsObject()["icrc1_transfer"]
const argsCodec = didToDisplayCodec(IDL.Tuple(...argTypes))
// The schema validates `unknown`, so the form's values are typed `unknown` too
const defaultValues: unknown = methodMeta.defaults
// A hook: call it from a component
function useTransferForm() {
return useForm({
defaultValues,
validators: { onBlur: methodMeta.schema },
onSubmit: async ({ value }) => {
// Principal, bigint, [] or [x] for an opt, { Tag: payload } for a variant.
// An untyped codec returns `unknown`, so name the tuple the actor takes.
const args = argsCodec.asCandid(value) as Parameters<
typeof actor.icrc1_transfer
>
await actor.icrc1_transfer(...args)
},
})
}

Use MetadataReactor if you want to work directly from canister ID + client manager and avoid manual visitor wiring.