baleethai/filament-thailand-location-picker
Install the package and publish its assets:
composer require baleethai/filament-thailand-location-picker
php artisan vendor:publish --tag="thailand-plugin-config"
php artisan vendor:publish --tag="thailand-plugin-translations" # new in v1.1.0
php artisan migrate --seed # or the package's dedicated location seeder
Three things to look at first:
config/thailand-plugin.php — the locale key (name_th vs name_en data column) and, as of v1.1.0, enable_geo (show/hide the Region/Geo select).lang/vendor/thailand-plugin/{en,th}.json — the new UI label/placeholder translations. Skim these to see exactly which strings you can override.ProvinceSelect, DistrictSelect, SubdistrictSelect, ZipCodeSelect, plus the ThailandLocation composite field) — these are what you'll wire into forms.First use case: an address block on a Customer or Order resource form. Drop the composite field into the schema, save the four IDs, and you have a working Region → Province → District → Subdistrict → Zip Code cascade in one line.
Composite field for the common case. Use ThailandLocation when you want the whole cascade with default behaviour; use the individual *Select fields when you need to interleave your own fields or pre-fill from existing data.
use BaleeThai\FilamentThailandLocationPicker\Forms\ThailandLocation;
ThailandLocation::make('address')
->label(__('customer.address'))
->required(),
Flat columns vs. relations. The simplest integration stores province_id, district_id, subdistrict_id, zip_code as integer/string columns on the parent model. If you need reporting or joins, add belongsTo relations (province(), district(), subdistrict()) and eager-load them in table queries.
Locale is two independent axes — set both deliberately.
// config/thailand-plugin.php
'locale' => 'name_th', // which data column is displayed/returned
// UI labels follow the app locale, not the config:
App::setLocale('th'); // Region/Province/District labels switch to Thai
This split is the most useful thing about v1.1.0: a Thai-facing admin can see Thai labels while an English-facing storefront shows name_en data, or vice versa, without fighting a single global setting.
Disabling Geo per form. When an address form doesn't need the region step (or the region is implied by context — e.g. a single-region warehouse), turn it off locally:
ThailandLocation::make('address')
->geoEnabled(false); // ProvinceSelect then lists all provinces unfiltered
Keep config('thailand-plugin.enable_geo') as the app-wide default and use ->geoEnabled() only where the form genuinely differs. Mixing both aggressively makes behaviour hard to reason about during review.
Translations. Publish once, then edit the JSON files under lang/vendor/thailand-plugin/. Override only the keys you care about — Laravel's JSON translation falls back per-key, so a sparse file still works for the rest.
{ "Province": "จังหวัด", "Zip Code": "รหัสไปรษณีย์" }
Display in tables/infolists. Don't render raw IDs. Either eager-load the relation and use ->formatStateUsing(fn ($state, $record) => $record->subdistrict?->{$column}), or add a model accessor that respects config('thailand-plugin.locale') so table and form stay consistent.
thailand-plugin.locale does not affect the new UI translations. If labels stay English in a Thai app, check App::getLocale() and the published JSON, not the config file.region_id when Geo is off. With enable_geo => false or ->geoEnabled(false), the Region input disappears from the form but the column still exists. Either ->dehydrated(false) the region field, null it in a mutateFormDataBeforeSave, or scope the province list — otherwise old records keep a region that no longer matches what the user sees.enable_geo or editing published JSON, run php artisan optimize:clear (or at minimum config:clear / cache:clear). Cached config is the usual reason a toggle "doesn't work".vendor:publish --tag="thailand-plugin-translations" after an upgrade will clobber your edits unless you pass --force knowingly. Commit the published files to version control.zip_code as the key for the cascade — drive the selects from IDs and treat zip as display/validation only.*Select fields yourself you own that reset logic (->live() + afterStateUpdated); the composite ThailandLocation field handles it for you — prefer it unless you need custom ordering.province/district columns without ->with(['province','district']) is a classic 100-query page. Eager-load, or denormalise the display name onto the parent row at save time.*Select classes are the seam: subclass or compose them with your own options() / searchable() / preload() configuration rather than forking the package.How can I help you explore Laravel packages today?