Skip to main content
Version: 4.0 preview

Countryselect, stateselect, countyselect

Version
4.0 preview
Updated
View markdown

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

TypeSettingMeaning
countryselectstateFieldNameThe property holding the state picker it drives
countryselectdefaultlabelPlaceholder, e.g. "Choose Country"
stateselectcountryFieldNameThe country property it narrows by
countyselectstateFieldNameThe 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:

$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:

'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 — see billing_city_id on the customer model — because there is nothing below it to cascade into.

@see join.md