# Countryselect, stateselect, countyselect

> Three cascading pickers for the country → state → county hierarchy. Each stores the referenced record's id.

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/property-types/geography-selects. Last updated 2026-07-27.

---
Three cascading pickers for the country → state → county hierarchy. Each stores the referenced
record's **id**.

**Storage kind:** `column` · **Column:** `int unsigned` — but see the caveat below

Use them for any address-like field. They exist as three types rather than one because each knows
which of the others it narrows and is narrowed by, and that wiring is what makes the cascade work.

## Settings

| Type | Setting | Meaning |
|---|---|---|
| `countryselect` | `stateFieldName` | The property holding the state picker it drives |
| `countryselect` | `defaultlabel` | Placeholder, e.g. "Choose Country" |
| `stateselect` | `countryFieldName` | The country property it narrows by |
| `countyselect` | `stateFieldName` | The state property it narrows by |

The pointers are what make the cascade work: choosing a country repopulates the state picker named
by `stateFieldName`, and the state picker knows which country to ask about from `countryFieldName`.
A typical trio, from `admincustomers`:

```php
$propDefs['billing_country_id']   = array(…, 'type' => 'countryselect', 'stateFieldName'   => 'billing_state_id');
$propDefs['billing_state_id']     = array(…, 'type' => 'stateselect',   'countryFieldName' => 'billing_country_id');
$propDefs['billing_county_id'] = array(…, 'type' => 'countyselect',  'stateFieldName'   => 'billing_state_id');
```

The names must match actual property names on the same model. Nothing validates them, so a typo
gives you a picker that never repopulates.

## Declare `dataType` and `nullable` explicitly

Only `stateselect` and `countyselect` declare `int unsigned` as their default column type.
**`countryselect` does not** — it inherits the base `varchar(255)`.

That is why every shipped definition spells both keys out:

```php
'name' => 'billing_country_id', 'nullable' => true, 'dataType' => 'int unsigned',
```

Do the same. Relying on the type's default gives you a `varchar(255)` column holding integers for
the country and `int unsigned` for the state — an inconsistency that only shows up when something
joins or compares them.

## They store ids, but they are not `number` fields

The value is an integer id, and it would be technically accurate to declare these as `number` with
`numberType => 'integer'`. Do not: the picker, the cascade and the label lookup are the point, and a
`number` field is a text input.

This came up when auditing types against their columns — six geography properties sit on
`int unsigned` columns with a non-numeric type, and that is correct.

## Notes

- An empty selection is stored as SQL `NULL`, not `0`. `countryselect` and `countyselect` read the
  request as an int and map anything empty to NULL; `stateselect` reads a string and maps the
  literal `'0'`. Either way, "nothing chosen" is NULL — declare the column `nullable`.
- These are heavily nullable in practice: on a live install nearly every customer row had NULL for
  `shipping_county_id` and `shipping_state_id`, because most stores never collect them.
- There is no `cityselect`. A city is a plain [`join`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/join) — see `billing_city_id` on the
  customer model — because there is nothing below it to cascade into.

@see join.md
