# Field Component Patterns

This document outlines patterns for creating and configuring field components in componentLibrary.

## Field Components Pattern

Field components destructure props inline with defaults for customization:

```typescript
const ContentFields = ({
  // Each field gets label, name, and default props
  fieldLabel = 'Default label',
  fieldName = 'defaultName',
  fieldDefault = 'Default value',
}: ContentFieldsProps) => {
  return (
    <>
      <TextField
        label={fieldLabel}
        name={fieldName}
        default={fieldDefault}
      />
      {/* Additional fields */}
    </>
  );
};

export default ContentFields;
```

## Subcomponent Field Placement

**Rule:** Define fields on the subcomponent that consumes them.

**Anti-pattern:** Rolling fields up to a parent component for "convenience" or centralization.

### Reference Example: Accordion

| Component | Fields | Reason |
|-----------|--------|--------|
| `Accordion` | `StyleFields` (gap) | Parent layout concern |
| `AccordionItem` | `StyleFields` (variant) | Item-level styling |
| `AccordionTitle` | `ContentFields` (title, icon) | Title-specific content |
| `AccordionContent` | `ContentFields` (content) | Content-specific field |

**File Structure:**
```
Accordion/
├── index.tsx
├── types.ts              # Accordion StyleFieldsProps (gap)
├── AccordionItem/
│   ├── StyleFields.tsx   # variant field
│   └── types.ts
├── AccordionTitle/
│   ├── ContentFields.tsx # title, icon fields
│   └── types.ts
└── AccordionContent/
    ├── ContentFields.tsx # content field
    └── types.ts
```

### Field Ordering with Multiple Subcomponents

When a module uses multiple subcomponents, order fields as follows:
1. Parent component fields first
2. Child component fields in DOM/render order
3. Group fields by subcomponent for clarity

**Code Example (module fields.tsx):**
```typescript
import {
  FieldGroup,
  ModuleFields,
  RepeatedFieldGroup,
} from '@hubspot/cms-components/fields';
import {
  AccordionContent,
  AccordionItem,
  AccordionTitle,
} from '../../componentLibrary/Accordion/index.js';

export const fields = (
  <ModuleFields>
    {/* Content fields in DOM/render order */}
    <RepeatedFieldGroup
      label="Accordion items"
      name="accordionItems"
      occurrence={{ min: 1, max: 20, default: 3 }}
    >
      <AccordionTitle.ContentFields />
      <AccordionContent.ContentFields />
    </RepeatedFieldGroup>
    {/* Style fields grouped in STYLE tab */}
    <FieldGroup label="Style" name="style" tab="STYLE">
      <AccordionItem.StyleFields />
    </FieldGroup>
  </ModuleFields>
);
```

## Field Label Casing

**Label formatting:**
All field labels should use sentence case, not title case.

- ✅ Correct: "Button text", "Background color", "Show icon"
- ❌ Incorrect: "Button Text", "Background Color", "Show Icon"

## Field Property Ordering

**Ordering of field properties:**
Use the following link to guide you: https://product.hubteam.com/docs/cms-asset-guidelines/hubl-modules.html#fieldsjson

Field properties should be ordered in the following order:
- label:
- name:
- id:
- type:
- tab:
- help_text:
- visibility_rules:
- advanced_visibility:
- visibility:
- children:
- aliases_mapping:
- inheritance:
- default:

## Field Component Props Destructuring

All field components destructure props inline in the function signature with defaults:

```typescript
const ContentFields = ({
  fieldLabel = 'Default label',
  fieldName = 'defaultName',
  fieldDefault = 'default value',
}: ContentFieldsProps) => {
  return <Field label={fieldLabel} name={fieldName} default={fieldDefault} />;
};

export default ContentFields;
```

## Common Field Patterns

### ChoiceField Pattern
When using ChoiceFields, choices are defined as nested arrays: `[value, displayLabel]`

```typescript
<ChoiceField
  label="Orientation"
  name="orientation"
  required={true}
  choices={[
    ['horizontal', 'Horizontal'],
    ['vertical', 'Vertical'],
  ]}
  default="horizontal"
/>
```

### NumberField Pattern with Units
NumberFields commonly include `suffix`, `min`, and `max` props:

```typescript
<NumberField
  label="Length"
  name="length"
  required={true}
  suffix="%"
  min={1}
  max={100}
  default={100}
/>
```

### Visibility Rules Pattern
Icon component demonstrates conditional field visibility using visibility rules:

```typescript
const SHOW_ICON_VISIBILITY_RULES: Visibility = {
  operator: 'EQUAL',
  controlling_field: 'iconComponentShowIcon',
  controlling_value_regex: 'true',
};

<IconField
  label={iconLabel}
  name={iconName}
  visibility={addIconToggle ? SHOW_ICON_VISIBILITY_RULES : undefined}
  default={iconDefault}
/>
```

**Key Pattern:** Use `id` on controlling field and reference it in `controlling_field`:
```typescript
<BooleanField
  id="iconComponentShowIcon"  // Referenced by other fields
  label={showIconLabel}
  name={showIconName}
  display="toggle"
/>
```

### Field Visibility Map Pattern

For components with configurable field names, use a `fieldVisibility` prop keyed by field name. Values match the cms-components `Visibility` schema (e.g. `hidden_subfields`, `controlling_field_path`, `operator`). Keys must match the field's name prop (e.g. `linkName`).

Link component example:

```typescript
<Link.ContentFields
  fieldVisibility={{ link: { hidden_subfields: { no_follow: true } } }}
/>

<Link.ContentFields
  linkName="footerLink"
  fieldVisibility={{ footerLink: { hidden_subfields: { no_follow: true, open_in_new_tab: true } } }}
/>
```

**Key Pattern:** Look up visibility by the configurable name prop: `visibility={fieldVisibility?.[linkName]}`

#### Type-Safe fieldVisibility with Discriminated Unions

Use a discriminated union to ensure `fieldVisibility` keys always match the runtime field name. When `linkName` is omitted, keys are locked to the default. When provided, keys must match it.

**Single-field component:**
```typescript
// types.ts
export type ContentFieldsProps<TLinkName extends string = 'link'> = {
  linkLabel?: string;
  linkDefault?: typeof LinkFieldDefaults;
} & (
  | { linkName?: never; fieldVisibility?: Partial<Record<'link', Visibility>> }
  | { linkName: TLinkName; fieldVisibility?: Partial<Record<TLinkName, Visibility>> }
);
```

This prevents TS from inferring `TLinkName` from `fieldVisibility` when `linkName` is omitted:
```typescript
// linkName omitted — fieldVisibility keys must be 'link'
<Link.ContentFields
  fieldVisibility={{ wrongKey: { ... } }}  // TS error: 'wrongKey' is not assignable to 'link'
/>

// linkName provided — fieldVisibility keys must match
<Link.ContentFields
  linkName="footerLink"
  fieldVisibility={{ link: { ... } }}  // TS error: 'link' is not assignable to 'footerLink'
/>
```

**Multi-field component (e.g. Button):**
```typescript
export type ContentFieldsProps<
  TTextName extends string = 'buttonText',
  TLinkName extends string = 'buttonLink',
> = {
  buttonTextLabel?: string;
  buttonLinkLabel?: string;
} & (
  | { buttonTextName?: never; buttonLinkName?: never;
      fieldVisibility?: Partial<Record<'buttonText' | 'buttonLink', Visibility>> }
  | { buttonTextName: TTextName; buttonLinkName: TLinkName;
      fieldVisibility?: Partial<Record<TTextName | TLinkName, Visibility>> }
);
```

## Nested Component Fields Pattern

**Pattern for nested component fields:**
```typescript
// Pass through props to nested component fields
<Icon.ContentFields
  addIconToggle={true}
  {...iconContentFieldsProps}
/>
```

## Complete Field Component Example

```typescript
// ContentFields.tsx
import { TextField } from '@hubspot/cms-components/fields';
import { ContentFieldsProps } from './types.js';

const ContentFields = ({
  fieldLabel = 'Field',
  fieldName = 'field',
  fieldDefault = 'Default',
}: ContentFieldsProps) => {
  return (
    <TextField
      label={fieldLabel}
      name={fieldName}
      default={fieldDefault}
    />
  );
};

export default ContentFields;
```
