Forms

CountryPicker

A drop-down of every country with its flag and ISO code, favourites first, sending the two-letter code with a form.

#forms#select#picker#no-js

Demo

Preview

The flag is the browser's emoji.

Shipping to Japan (JP).

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/country_picker.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/country_picker.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/country_picker.qwe
component CountryPickerDemoLive() {    state code = "JP"    <div.demo-stack>        <CountryPicker name="ship_to" label="Ship to" value={code} field="country-picker-demo-live"                       only={["US", "CA", "GB", "DE", "FR", "JP", "AU", "BR"]} codes={false}                       on_change={(c) => { code = c }}/>        <p.demo-note>Shipping to {country_picker_name(code)} ({code}).</p>    </div>}view CountryPickerDemo() =    <div.demo-stack.demo-narrow>        <CountryPicker name="country" label="Country" value="PT" field="country-picker-demo-all"                       favourites={["PT", "ES", "GB", "US"]} hint="The flag is the browser's emoji." required={true}/>        <CountryPickerDemoLive/>        <div.demo-row>            <CountryPicker name="market" label="Market" size="sm" placeholder="Choose a market" field="country-picker-demo-small"                           only={["DE", "FR", "ES", "IT"]} flags={false}/>            <CountryPicker name="locked" label="Disabled" size="sm" value="CH" only={["CH"]} disabled={true}/>        </div>    </div>

Source

ui/country_picker.qwe
// CountryPicker: a drop-down of countries, each with its flag (emoji) and ISO// 3166-1 code. It is the browser's own <select>, so a plain <form> sends the// two-letter code ("PT") as `name`, and it works with the keyboard.////   <CountryPicker name="country" label="Country" value="PT"/>//   <CountryPicker name="ship_to" label="Ship to" favourites={["US", "CA", "GB"]} placeholder="Choose a country"/>//   <CountryPicker name="market" label="Market" only={["DE", "FR", "ES"]} codes={false}/>//// value: the code chosen to start with. favourites: codes listed first, under// "Common"; the rest come under "All countries". only: the codes to offer, in// place of the full list of 249 (`country_picker_countries`, a list of [code, name]// pairs). flags (true): the flag before each name. codes (true): the code after// it ("🇵🇹 Portugal (PT)"). placeholder: a first line that cannot be chosen.// hint, error, required, disabled, field (the id, default "country-picker-" + name)// and size ("sm" or "md") as in Select.// on_change(code): inside one of your components, given the code chosen.import ui/theme.qwe use ui_clslet country_picker_css = css`.ui-country-picker { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-country-picker-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-country-picker-req { margin-left: 2px; color: var(--ui-danger); }.ui-country-picker-box { position: relative; display: block; }.ui-country-picker-box::after {  content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; margin-top: -6px;  border: solid var(--ui-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); pointer-events: none;}.ui-country-picker select {  appearance: none; display: block; width: 100%; height: 38px; padding: 0 36px 0 12px;  background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font);  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); cursor: pointer;  text-overflow: ellipsis; transition: border-color .15s, box-shadow .15s;}.ui-country-picker select:hover { border-color: var(--ui-strong); }.ui-country-picker select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-country-picker select:has(option[value=""]:checked) { color: var(--ui-muted); }.ui-country-picker option { color: var(--ui-fg); background: var(--ui-surface); }.ui-country-picker-sm select { height: 30px; padding: 0 30px 0 10px; font-size: 13px; }.ui-country-picker-sm .ui-country-picker-box::after { right: 11px; width: 6px; height: 6px; margin-top: -5px; }.ui-country-picker-invalid select, .ui-country-picker-invalid select:hover { border-color: var(--ui-danger); }.ui-country-picker-hint, .ui-country-picker-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-country-picker-error { color: var(--ui-danger); }.ui-country-picker-disabled { opacity: .55; }.ui-country-picker-disabled select { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-country-picker select { transition: none; } }`// Every country and territory of ISO 3166-1, as [alpha-2 code, short name].let country_picker_countries = [    ["AF", "Afghanistan"], ["AX", "Åland Islands"], ["AL", "Albania"], ["DZ", "Algeria"], ["AS", "American Samoa"],    ["AD", "Andorra"], ["AO", "Angola"], ["AI", "Anguilla"], ["AQ", "Antarctica"], ["AG", "Antigua and Barbuda"],    ["AR", "Argentina"], ["AM", "Armenia"], ["AW", "Aruba"], ["AU", "Australia"], ["AT", "Austria"], ["AZ", "Azerbaijan"],    ["BS", "Bahamas"], ["BH", "Bahrain"], ["BD", "Bangladesh"], ["BB", "Barbados"], ["BY", "Belarus"], ["BE", "Belgium"],    ["BZ", "Belize"], ["BJ", "Benin"], ["BM", "Bermuda"], ["BT", "Bhutan"], ["BO", "Bolivia"],    ["BQ", "Bonaire, Sint Eustatius and Saba"], ["BA", "Bosnia and Herzegovina"], ["BW", "Botswana"], ["BV", "Bouvet Island"],    ["BR", "Brazil"], ["IO", "British Indian Ocean Territory"], ["BN", "Brunei"], ["BG", "Bulgaria"], ["BF", "Burkina Faso"],    ["BI", "Burundi"], ["CV", "Cabo Verde"], ["KH", "Cambodia"], ["CM", "Cameroon"], ["CA", "Canada"], ["KY", "Cayman Islands"],    ["CF", "Central African Republic"], ["TD", "Chad"], ["CL", "Chile"], ["CN", "China"], ["CX", "Christmas Island"],    ["CC", "Cocos (Keeling) Islands"], ["CO", "Colombia"], ["KM", "Comoros"], ["CG", "Congo"],    ["CD", "Congo (Democratic Republic)"], ["CK", "Cook Islands"], ["CR", "Costa Rica"], ["CI", "Côte d'Ivoire"], ["HR", "Croatia"],    ["CU", "Cuba"], ["CW", "Curaçao"], ["CY", "Cyprus"], ["CZ", "Czechia"], ["DK", "Denmark"], ["DJ", "Djibouti"],    ["DM", "Dominica"], ["DO", "Dominican Republic"], ["EC", "Ecuador"], ["EG", "Egypt"], ["SV", "El Salvador"],    ["GQ", "Equatorial Guinea"], ["ER", "Eritrea"], ["EE", "Estonia"], ["SZ", "Eswatini"], ["ET", "Ethiopia"],    ["FK", "Falkland Islands"], ["FO", "Faroe Islands"], ["FJ", "Fiji"], ["FI", "Finland"], ["FR", "France"],    ["GF", "French Guiana"], ["PF", "French Polynesia"], ["TF", "French Southern Territories"], ["GA", "Gabon"],    ["GM", "Gambia"], ["GE", "Georgia"], ["DE", "Germany"], ["GH", "Ghana"], ["GI", "Gibraltar"], ["GR", "Greece"],    ["GL", "Greenland"], ["GD", "Grenada"], ["GP", "Guadeloupe"], ["GU", "Guam"], ["GT", "Guatemala"], ["GG", "Guernsey"],    ["GN", "Guinea"], ["GW", "Guinea-Bissau"], ["GY", "Guyana"], ["HT", "Haiti"], ["HM", "Heard Island and McDonald Islands"],    ["VA", "Holy See"], ["HN", "Honduras"], ["HK", "Hong Kong"], ["HU", "Hungary"], ["IS", "Iceland"], ["IN", "India"],    ["ID", "Indonesia"], ["IR", "Iran"], ["IQ", "Iraq"], ["IE", "Ireland"], ["IM", "Isle of Man"], ["IL", "Israel"],    ["IT", "Italy"], ["JM", "Jamaica"], ["JP", "Japan"], ["JE", "Jersey"], ["JO", "Jordan"], ["KZ", "Kazakhstan"],    ["KE", "Kenya"], ["KI", "Kiribati"], ["KP", "Korea (North)"], ["KR", "Korea (South)"], ["KW", "Kuwait"],    ["KG", "Kyrgyzstan"], ["LA", "Laos"], ["LV", "Latvia"], ["LB", "Lebanon"], ["LS", "Lesotho"], ["LR", "Liberia"],    ["LY", "Libya"], ["LI", "Liechtenstein"], ["LT", "Lithuania"], ["LU", "Luxembourg"], ["MO", "Macao"],    ["MG", "Madagascar"], ["MW", "Malawi"], ["MY", "Malaysia"], ["MV", "Maldives"], ["ML", "Mali"], ["MT", "Malta"],    ["MH", "Marshall Islands"], ["MQ", "Martinique"], ["MR", "Mauritania"], ["MU", "Mauritius"], ["YT", "Mayotte"],    ["MX", "Mexico"], ["FM", "Micronesia"], ["MD", "Moldova"], ["MC", "Monaco"], ["MN", "Mongolia"], ["ME", "Montenegro"],    ["MS", "Montserrat"], ["MA", "Morocco"], ["MZ", "Mozambique"], ["MM", "Myanmar"], ["NA", "Namibia"], ["NR", "Nauru"],    ["NP", "Nepal"], ["NL", "Netherlands"], ["NC", "New Caledonia"], ["NZ", "New Zealand"], ["NI", "Nicaragua"],    ["NE", "Niger"], ["NG", "Nigeria"], ["NU", "Niue"], ["NF", "Norfolk Island"], ["MK", "North Macedonia"],    ["MP", "Northern Mariana Islands"], ["NO", "Norway"], ["OM", "Oman"], ["PK", "Pakistan"], ["PW", "Palau"],    ["PS", "Palestine"], ["PA", "Panama"], ["PG", "Papua New Guinea"], ["PY", "Paraguay"], ["PE", "Peru"],    ["PH", "Philippines"], ["PN", "Pitcairn"], ["PL", "Poland"], ["PT", "Portugal"], ["PR", "Puerto Rico"], ["QA", "Qatar"],    ["RE", "Réunion"], ["RO", "Romania"], ["RU", "Russia"], ["RW", "Rwanda"], ["BL", "Saint Barthélemy"],    ["SH", "Saint Helena, Ascension and Tristan da Cunha"], ["KN", "Saint Kitts and Nevis"], ["LC", "Saint Lucia"],    ["MF", "Saint Martin"], ["PM", "Saint Pierre and Miquelon"], ["VC", "Saint Vincent and the Grenadines"],    ["WS", "Samoa"], ["SM", "San Marino"], ["ST", "Sao Tome and Principe"], ["SA", "Saudi Arabia"], ["SN", "Senegal"],    ["RS", "Serbia"], ["SC", "Seychelles"], ["SL", "Sierra Leone"], ["SG", "Singapore"], ["SX", "Sint Maarten"],    ["SK", "Slovakia"], ["SI", "Slovenia"], ["SB", "Solomon Islands"], ["SO", "Somalia"], ["ZA", "South Africa"],    ["GS", "South Georgia and the South Sandwich Islands"], ["SS", "South Sudan"], ["ES", "Spain"], ["LK", "Sri Lanka"],    ["SD", "Sudan"], ["SR", "Suriname"], ["SJ", "Svalbard and Jan Mayen"], ["SE", "Sweden"], ["CH", "Switzerland"],    ["SY", "Syria"], ["TW", "Taiwan"], ["TJ", "Tajikistan"], ["TZ", "Tanzania"], ["TH", "Thailand"], ["TL", "Timor-Leste"],    ["TG", "Togo"], ["TK", "Tokelau"], ["TO", "Tonga"], ["TT", "Trinidad and Tobago"], ["TN", "Tunisia"], ["TR", "Türkiye"],    ["TM", "Turkmenistan"], ["TC", "Turks and Caicos Islands"], ["TV", "Tuvalu"], ["UG", "Uganda"], ["UA", "Ukraine"],    ["AE", "United Arab Emirates"], ["GB", "United Kingdom"], ["US", "United States"],    ["UM", "United States Minor Outlying Islands"], ["UY", "Uruguay"], ["UZ", "Uzbekistan"], ["VU", "Vanuatu"],    ["VE", "Venezuela"], ["VN", "Vietnam"], ["VG", "Virgin Islands (British)"], ["VI", "Virgin Islands (U.S.)"],    ["WF", "Wallis and Futuna"], ["EH", "Western Sahara"], ["YE", "Yemen"], ["ZM", "Zambia"], ["ZW", "Zimbabwe"],]// The regional indicator letter for each capital, which make the flags in pairs.let country_picker_letters = {    A: "\u{1F1E6}", B: "\u{1F1E7}", C: "\u{1F1E8}", D: "\u{1F1E9}", E: "\u{1F1EA}", F: "\u{1F1EB}", G: "\u{1F1EC}",    H: "\u{1F1ED}", I: "\u{1F1EE}", J: "\u{1F1EF}", K: "\u{1F1F0}", L: "\u{1F1F1}", M: "\u{1F1F2}", N: "\u{1F1F3}",    O: "\u{1F1F4}", P: "\u{1F1F5}", Q: "\u{1F1F6}", R: "\u{1F1F7}", S: "\u{1F1F8}", T: "\u{1F1F9}", U: "\u{1F1FA}",    V: "\u{1F1FB}", W: "\u{1F1FC}", X: "\u{1F1FD}", Y: "\u{1F1FE}", Z: "\u{1F1FF}",}// The flag emoji of a two-letter code: country_picker_flag("PT") is "🇵🇹".fn country_picker_flag(code: str) -> str {    let c = upper(code)    (country_picker_letters[c[0]] ?? "") + (country_picker_letters[c[1]] ?? "")}// The name of a code ("Portugal" for "PT"), or the code itself when it is not known.fn country_picker_name(code: str) -> str {    let c = upper(code)    let hit = find(country_picker_countries, (p) => p[0] == c)    hit != null ? hit[1] : code}view CountryPicker(name: str? = null, label: str? = null, value: str? = null, favourites: list = [], only: list? = null,                   flags: bool = true, codes: bool = true, placeholder: str? = null, hint: str? = null,                   error: str? = null, required: bool = false, disabled: bool = false, field: str? = null,                   size: str = "md", on_change = null, class: str? = null) {    style global country_picker_css    let id = field ?? `country-picker-${name ?? "country"}`    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`])    let chosen = value != null ? upper(value) : null    let wanted = only != null ? only | map((c) => upper(c)) : null    let all = country_picker_countries | filter((p) => wanted == null || contains(wanted, p[0]))    let favs = favourites | map((c) => upper(c))    let top = favs | map((c) => find(all, (p) => p[0] == c)) | filter((p) => p != null)    let rest = len(top) > 0 ? all | filter((p) => !contains(favs, p[0])) : all    let opt = (p) => <option value={p[0]} selected={p[0] == chosen}>        {(flags ? country_picker_flag(p[0]) + " " : "") + p[1] + (codes ? ` (${p[0]})` : "")}    </option>    <div class={ui_cls(["ui-country-picker", size == "sm" && "ui-country-picker-sm", error != null && "ui-country-picker-invalid",                        disabled && "ui-country-picker-disabled", class])}>        {label != null && <label.ui-country-picker-label for={id}>            {label}{required && <span.ui-country-picker-req aria-hidden="true">*</span>}        </label>}        <span.ui-country-picker-box>            <select id={id} name={name} required={required} disabled={disabled} autocomplete="country"                    aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}                    @change={(e) => { on_change && on_change(e.value) }}>                {placeholder != null && <option value="" disabled selected={chosen == null}>{placeholder}</option>}                {len(top) > 0                    ? [<optgroup label="Common">{for p in top => opt(p)}</optgroup>,                       <optgroup label="All countries">{for p in rest => opt(p)}</optgroup>]                    : (for p in rest => opt(p))}            </select>        </span>        {hint != null && <p.ui-country-picker-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-country-picker-error id={`${id}-error`} role="alert">{error}</p>}    </div>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

On CountryPicker: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.