country-data-filter 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -7
- package/package.json +1 -1
- package/src/angular/{location-picker.component.ts → dropdown/location-picker.component.ts} +12 -18
- package/src/angular/index.ts +4 -2
- package/src/angular/location-picker.types.ts +3 -0
- package/src/angular/select/location-picker.component.html +62 -0
- package/src/angular/select/location-picker.component.ts +224 -0
- /package/src/angular/{location-picker.component.html → dropdown/location-picker.component.html} +0 -0
package/README.md
CHANGED
|
@@ -484,7 +484,16 @@ The dropdowns shown are determined by which `on*Change` callbacks are provided:
|
|
|
484
484
|
|
|
485
485
|
## Angular Location Picker
|
|
486
486
|
|
|
487
|
-
A standalone PrimeNG cascade picker
|
|
487
|
+
A standalone PrimeNG cascade picker for Angular 17+. Ships as raw TypeScript source — no build step required.
|
|
488
|
+
|
|
489
|
+
### PrimeNG version compatibility
|
|
490
|
+
|
|
491
|
+
| PrimeNG version | Component to import | Selector |
|
|
492
|
+
|---|---|---|
|
|
493
|
+
| v17 – v19 | `LocationPickerComponent` | `<cdf-location-picker>` |
|
|
494
|
+
| v20+ | `LocationPickerSelectComponent` | `<cdf-location-picker-select>` |
|
|
495
|
+
|
|
496
|
+
PrimeNG v20 removed `primeng/dropdown` (`p-dropdown`). The `LocationPickerSelectComponent` uses `primeng/select` (`p-select`) which is required for v18+.
|
|
488
497
|
|
|
489
498
|
### Peer dependencies
|
|
490
499
|
|
|
@@ -507,18 +516,36 @@ Add the source path to your `tsconfig.json`:
|
|
|
507
516
|
|
|
508
517
|
### Import
|
|
509
518
|
|
|
519
|
+
Both components are exported from the main barrel. Import by name — TypeScript will resolve the correct file:
|
|
520
|
+
|
|
521
|
+
**PrimeNG v17–v19:**
|
|
522
|
+
|
|
510
523
|
```ts
|
|
511
524
|
import { LocationPickerComponent } from 'country-data-filter/angular';
|
|
512
525
|
```
|
|
513
526
|
|
|
514
|
-
|
|
527
|
+
**PrimeNG v20+:**
|
|
515
528
|
|
|
516
529
|
```ts
|
|
530
|
+
import { LocationPickerSelectComponent } from 'country-data-filter/angular';
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Add to your component's `imports` array (both are standalone components):
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
// PrimeNG v17-v19
|
|
517
537
|
@Component({
|
|
518
538
|
standalone: true,
|
|
519
539
|
imports: [LocationPickerComponent],
|
|
520
540
|
template: `<cdf-location-picker [countryControl]="countryCtrl" />`
|
|
521
541
|
})
|
|
542
|
+
|
|
543
|
+
// PrimeNG v20+
|
|
544
|
+
@Component({
|
|
545
|
+
standalone: true,
|
|
546
|
+
imports: [LocationPickerSelectComponent],
|
|
547
|
+
template: `<cdf-location-picker-select [countryControl]="countryCtrl" />`
|
|
548
|
+
})
|
|
522
549
|
```
|
|
523
550
|
|
|
524
551
|
### Basic usage — reactive forms
|
|
@@ -587,7 +614,7 @@ Passing `[cityControl]` automatically implies all levels above it.
|
|
|
587
614
|
|
|
588
615
|
### PrimeNG prop passthrough
|
|
589
616
|
|
|
590
|
-
|
|
617
|
+
**v17–v19** — use `*DropdownProps` inputs:
|
|
591
618
|
|
|
592
619
|
```html
|
|
593
620
|
<cdf-location-picker
|
|
@@ -597,14 +624,30 @@ Pass any PrimeNG `p-dropdown` props via the `*DropdownProps` inputs:
|
|
|
597
624
|
</cdf-location-picker>
|
|
598
625
|
```
|
|
599
626
|
|
|
627
|
+
**v20+** — use `*SelectProps` inputs (same object shape):
|
|
628
|
+
|
|
629
|
+
```html
|
|
630
|
+
<cdf-location-picker-select
|
|
631
|
+
[countryControl]="countryCtrl"
|
|
632
|
+
[countrySelectProps]="{ styleClass: 'w-full', appendTo: 'body', showClear: true }"
|
|
633
|
+
[provinceSelectProps]="{ styleClass: 'w-full', filter: false }">
|
|
634
|
+
</cdf-location-picker-select>
|
|
635
|
+
```
|
|
636
|
+
|
|
600
637
|
### Layout
|
|
601
638
|
|
|
602
639
|
```html
|
|
640
|
+
<!-- v17-v19 -->
|
|
603
641
|
<cdf-location-picker layout="horizontal" [countryControl]="ctrl" />
|
|
642
|
+
|
|
643
|
+
<!-- v20+ -->
|
|
644
|
+
<cdf-location-picker-select layout="horizontal" [countryControl]="ctrl" />
|
|
604
645
|
```
|
|
605
646
|
|
|
606
647
|
### Inputs reference
|
|
607
648
|
|
|
649
|
+
Both components share the same inputs — the only difference is `*DropdownProps` vs `*SelectProps`:
|
|
650
|
+
|
|
608
651
|
| Input | Type | Default | Description |
|
|
609
652
|
|---|---|---|---|
|
|
610
653
|
| `countryControl` | `AbstractControl` | — | Reactive form control for country |
|
|
@@ -613,10 +656,14 @@ Pass any PrimeNG `p-dropdown` props via the `*DropdownProps` inputs:
|
|
|
613
656
|
| `cityControl` | `AbstractControl` | — | Reactive form control for city |
|
|
614
657
|
| `countryLabelKey` | `'name' \| 'code' \| 'currency' \| 'countryCode'` | `'name'` | Field to use as dropdown label |
|
|
615
658
|
| `countryValueKey` | `'name' \| 'code' \| 'currency' \| 'countryCode'` | `'code'` | Field to use as option value |
|
|
616
|
-
| `countryDropdownProps` | `DropdownProps` | `{}` | Props spread onto the country `<p-dropdown>` |
|
|
617
|
-
| `provinceDropdownProps` | `DropdownProps` | `{}` | Props spread onto the province `<p-dropdown>` |
|
|
618
|
-
| `districtDropdownProps` | `DropdownProps` | `{}` | Props spread onto the district `<p-dropdown>` |
|
|
619
|
-
| `cityDropdownProps` | `DropdownProps` | `{}` | Props spread onto the city `<p-dropdown>` |
|
|
659
|
+
| `countryDropdownProps` *(v17–v19)* | `DropdownProps` | `{}` | Props spread onto the country `<p-dropdown>` |
|
|
660
|
+
| `provinceDropdownProps` *(v17–v19)* | `DropdownProps` | `{}` | Props spread onto the province `<p-dropdown>` |
|
|
661
|
+
| `districtDropdownProps` *(v17–v19)* | `DropdownProps` | `{}` | Props spread onto the district `<p-dropdown>` |
|
|
662
|
+
| `cityDropdownProps` *(v17–v19)* | `DropdownProps` | `{}` | Props spread onto the city `<p-dropdown>` |
|
|
663
|
+
| `countrySelectProps` *(v20+)* | `SelectProps` | `{}` | Props spread onto the country `<p-select>` |
|
|
664
|
+
| `provinceSelectProps` *(v20+)* | `SelectProps` | `{}` | Props spread onto the province `<p-select>` |
|
|
665
|
+
| `districtSelectProps` *(v20+)* | `SelectProps` | `{}` | Props spread onto the district `<p-select>` |
|
|
666
|
+
| `citySelectProps` *(v20+)* | `SelectProps` | `{}` | Props spread onto the city `<p-select>` |
|
|
620
667
|
| `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Flex direction of the wrapper |
|
|
621
668
|
|
|
622
669
|
### Outputs reference
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "country-data-filter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Geographical data for 190 countries — provinces, districts, cities and postal codes. Includes React (Ant Design / MUI) and Angular (PrimeNG) location picker cascade components with full TypeScript support.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -16,7 +16,7 @@ import { DropdownModule } from 'primeng/dropdown';
|
|
|
16
16
|
import { Observable, of, Subject } from 'rxjs';
|
|
17
17
|
import { startWith, switchMap, tap, takeUntil } from 'rxjs/operators';
|
|
18
18
|
|
|
19
|
-
import { LocationPickerService } from '
|
|
19
|
+
import { LocationPickerService } from '../location-picker.service';
|
|
20
20
|
import {
|
|
21
21
|
LocationOption,
|
|
22
22
|
CountryChangeEvent,
|
|
@@ -24,14 +24,20 @@ import {
|
|
|
24
24
|
CityChangeEvent,
|
|
25
25
|
DropdownProps,
|
|
26
26
|
CountryKey,
|
|
27
|
-
} from '
|
|
28
|
-
|
|
27
|
+
} from '../location-picker.types';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Location picker cascade component for PrimeNG v17–v19.
|
|
31
|
+
*
|
|
32
|
+
* Uses `<p-dropdown>` from `primeng/dropdown`.
|
|
33
|
+
* For PrimeNG v20+, use `LocationPickerSelectComponent` from `../select` instead.
|
|
34
|
+
*/
|
|
29
35
|
@Component({
|
|
30
36
|
selector: 'cdf-location-picker',
|
|
31
37
|
standalone: true,
|
|
32
38
|
imports: [CommonModule, ReactiveFormsModule, DropdownModule],
|
|
33
39
|
templateUrl: './location-picker.component.html',
|
|
34
|
-
styleUrls: ['
|
|
40
|
+
styleUrls: ['../location-picker.component.css'],
|
|
35
41
|
})
|
|
36
42
|
export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
37
43
|
// ── External reactive-form controls (all optional) ──────────────────────────
|
|
@@ -41,10 +47,10 @@ export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
|
41
47
|
@Input() cityControl?: AbstractControl;
|
|
42
48
|
|
|
43
49
|
// ── PrimeNG prop passthrough bags ────────────────────────────────────────────
|
|
44
|
-
@Input() countryDropdownProps:
|
|
50
|
+
@Input() countryDropdownProps: DropdownProps = {};
|
|
45
51
|
@Input() provinceDropdownProps: DropdownProps = {};
|
|
46
52
|
@Input() districtDropdownProps: DropdownProps = {};
|
|
47
|
-
@Input() cityDropdownProps:
|
|
53
|
+
@Input() cityDropdownProps: DropdownProps = {};
|
|
48
54
|
|
|
49
55
|
// ── Country option key selectors ─────────────────────────────────────────────
|
|
50
56
|
/** Which field of the country record to use as the dropdown label. Default: 'name' */
|
|
@@ -92,12 +98,10 @@ export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
|
92
98
|
constructor(private svc: LocationPickerService) {}
|
|
93
99
|
|
|
94
100
|
ngOnInit(): void {
|
|
95
|
-
// Compute which dropdowns to show
|
|
96
101
|
this.showCity = !!this.cityControl;
|
|
97
102
|
this.showDistrict = !!this.districtControl || this.showCity;
|
|
98
103
|
this.showProvince = !!this.provinceControl || this.showDistrict;
|
|
99
104
|
|
|
100
|
-
// ── Countries (static, sorted A-Z) ──────────────────────────────────────
|
|
101
105
|
const allCountries = this.svc.getCountries()
|
|
102
106
|
.map(c => ({
|
|
103
107
|
label: (c as any)[this.countryLabelKey] as string,
|
|
@@ -106,14 +110,12 @@ export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
|
106
110
|
.sort((a, b) => a.label.localeCompare(b.label));
|
|
107
111
|
this.countryOptions$ = of(allCountries);
|
|
108
112
|
|
|
109
|
-
// Reverse map: mapped option value → raw country code (used for data lookups)
|
|
110
113
|
const mappedValueToCode = new Map<string, string>(
|
|
111
114
|
this.svc.getCountries().map(c => [
|
|
112
115
|
(c as any)[this.countryValueKey] as string,
|
|
113
116
|
c.code,
|
|
114
117
|
])
|
|
115
118
|
);
|
|
116
|
-
// Forward map: raw code → mapped option value (used when auto-filling country)
|
|
117
119
|
const codeToMappedValue = new Map<string, string>(
|
|
118
120
|
this.svc.getCountries().map(c => [
|
|
119
121
|
c.code,
|
|
@@ -121,11 +123,9 @@ export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
|
121
123
|
])
|
|
122
124
|
);
|
|
123
125
|
|
|
124
|
-
// Helper: get raw code from whatever value is stored in the country control
|
|
125
126
|
const rawCode = (val: string | null): string | null =>
|
|
126
127
|
val ? (mappedValueToCode.get(val) ?? val) : null;
|
|
127
128
|
|
|
128
|
-
// ── Province options (reload when country changes) ───────────────────────
|
|
129
129
|
this.provinceOptions$ = this._country.valueChanges.pipe(
|
|
130
130
|
takeUntil(this.destroy$),
|
|
131
131
|
startWith(this._country.value),
|
|
@@ -133,7 +133,6 @@ export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
|
133
133
|
this._province.setValue(null, { emitEvent: false });
|
|
134
134
|
this._district.setValue(null, { emitEvent: false });
|
|
135
135
|
this._city.setValue(null, { emitEvent: false });
|
|
136
|
-
// Update placeholders from the selected country's labels
|
|
137
136
|
const code = rawCode(val);
|
|
138
137
|
if (code) {
|
|
139
138
|
const country = this.svc.getCountries().find(c => c.code === code);
|
|
@@ -154,18 +153,15 @@ export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
|
154
153
|
}),
|
|
155
154
|
);
|
|
156
155
|
|
|
157
|
-
// ── District options (reload when province changes; auto-fill country) ───
|
|
158
156
|
this.districtOptions$ = this._province.valueChanges.pipe(
|
|
159
157
|
takeUntil(this.destroy$),
|
|
160
158
|
startWith(this._province.value),
|
|
161
159
|
tap((name: string | null) => {
|
|
162
160
|
this._district.setValue(null, { emitEvent: false });
|
|
163
161
|
this._city.setValue(null, { emitEvent: false });
|
|
164
|
-
// Reverse lookup: if country not set, auto-fill from province name
|
|
165
162
|
if (name && !this._country.value) {
|
|
166
163
|
const code = this.svc.getCountryByProvince(name);
|
|
167
164
|
if (code) {
|
|
168
|
-
// Set the mapped value (not the raw code) so the control stays consistent
|
|
169
165
|
this._country.setValue(codeToMappedValue.get(code) ?? code);
|
|
170
166
|
}
|
|
171
167
|
}
|
|
@@ -178,7 +174,6 @@ export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
|
178
174
|
}),
|
|
179
175
|
);
|
|
180
176
|
|
|
181
|
-
// ── City options (reload when district changes) ───────────────────────────
|
|
182
177
|
this.cityOptions$ = this._district.valueChanges.pipe(
|
|
183
178
|
takeUntil(this.destroy$),
|
|
184
179
|
startWith(this._district.value),
|
|
@@ -192,7 +187,6 @@ export class LocationPickerComponent implements OnInit, OnDestroy {
|
|
|
192
187
|
}),
|
|
193
188
|
);
|
|
194
189
|
|
|
195
|
-
// ── Emit output events ───────────────────────────────────────────────────
|
|
196
190
|
this._country.valueChanges
|
|
197
191
|
.pipe(takeUntil(this.destroy$))
|
|
198
192
|
.subscribe((v: string | null) => {
|
package/src/angular/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export { LocationPickerComponent }
|
|
2
|
-
export {
|
|
1
|
+
export { LocationPickerComponent } from './dropdown/location-picker.component';
|
|
2
|
+
export { LocationPickerSelectComponent } from './select/location-picker.component';
|
|
3
|
+
export { LocationPickerService } from './location-picker.service';
|
|
3
4
|
export type {
|
|
4
5
|
LocationOption,
|
|
5
6
|
CountryChangeEvent,
|
|
6
7
|
NameChangeEvent,
|
|
7
8
|
CityChangeEvent,
|
|
8
9
|
DropdownProps,
|
|
10
|
+
SelectProps,
|
|
9
11
|
CountryKey,
|
|
10
12
|
} from './location-picker.types';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<div [class]="'cdf-location-picker cdf-' + layout">
|
|
2
|
+
|
|
3
|
+
<!-- Country select — always shown -->
|
|
4
|
+
<p-select
|
|
5
|
+
[formControl]="_country"
|
|
6
|
+
[options]="(countryOptions$ | async) ?? []"
|
|
7
|
+
[filter]="true"
|
|
8
|
+
[placeholder]="countrySelectProps['placeholder'] || 'Select Country'"
|
|
9
|
+
[styleClass]="countrySelectProps['styleClass']"
|
|
10
|
+
[showClear]="countrySelectProps['showClear'] ?? true"
|
|
11
|
+
[appendTo]="countrySelectProps['appendTo'] || 'body'"
|
|
12
|
+
[pt]="countrySelectProps['pt']"
|
|
13
|
+
optionLabel="label"
|
|
14
|
+
optionValue="value">
|
|
15
|
+
</p-select>
|
|
16
|
+
|
|
17
|
+
<!-- Province select -->
|
|
18
|
+
<p-select
|
|
19
|
+
*ngIf="showProvince"
|
|
20
|
+
[formControl]="_province"
|
|
21
|
+
[options]="(provinceOptions$ | async) ?? []"
|
|
22
|
+
[filter]="true"
|
|
23
|
+
[placeholder]="provinceSelectProps['placeholder'] || _provincePlaceholder"
|
|
24
|
+
[styleClass]="provinceSelectProps['styleClass']"
|
|
25
|
+
[showClear]="provinceSelectProps['showClear'] ?? true"
|
|
26
|
+
[appendTo]="provinceSelectProps['appendTo'] || 'body'"
|
|
27
|
+
[pt]="provinceSelectProps['pt']"
|
|
28
|
+
optionLabel="label"
|
|
29
|
+
optionValue="value">
|
|
30
|
+
</p-select>
|
|
31
|
+
|
|
32
|
+
<!-- District select -->
|
|
33
|
+
<p-select
|
|
34
|
+
*ngIf="showDistrict"
|
|
35
|
+
[formControl]="_district"
|
|
36
|
+
[options]="(districtOptions$ | async) ?? []"
|
|
37
|
+
[filter]="true"
|
|
38
|
+
[placeholder]="districtSelectProps['placeholder'] || _districtPlaceholder"
|
|
39
|
+
[styleClass]="districtSelectProps['styleClass']"
|
|
40
|
+
[showClear]="districtSelectProps['showClear'] ?? true"
|
|
41
|
+
[appendTo]="districtSelectProps['appendTo'] || 'body'"
|
|
42
|
+
[pt]="districtSelectProps['pt']"
|
|
43
|
+
optionLabel="label"
|
|
44
|
+
optionValue="value">
|
|
45
|
+
</p-select>
|
|
46
|
+
|
|
47
|
+
<!-- City select -->
|
|
48
|
+
<p-select
|
|
49
|
+
*ngIf="showCity"
|
|
50
|
+
[formControl]="_city"
|
|
51
|
+
[options]="(cityOptions$ | async) ?? []"
|
|
52
|
+
[filter]="true"
|
|
53
|
+
[placeholder]="citySelectProps['placeholder'] || 'Select City'"
|
|
54
|
+
[styleClass]="citySelectProps['styleClass']"
|
|
55
|
+
[showClear]="citySelectProps['showClear'] ?? true"
|
|
56
|
+
[appendTo]="citySelectProps['appendTo'] || 'body'"
|
|
57
|
+
[pt]="citySelectProps['pt']"
|
|
58
|
+
optionLabel="label"
|
|
59
|
+
optionValue="value">
|
|
60
|
+
</p-select>
|
|
61
|
+
|
|
62
|
+
</div>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Input,
|
|
4
|
+
Output,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
OnInit,
|
|
7
|
+
OnDestroy,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { CommonModule } from '@angular/common';
|
|
10
|
+
import {
|
|
11
|
+
AbstractControl,
|
|
12
|
+
FormControl,
|
|
13
|
+
ReactiveFormsModule,
|
|
14
|
+
} from '@angular/forms';
|
|
15
|
+
import { SelectModule } from 'primeng/select';
|
|
16
|
+
import { Observable, of, Subject } from 'rxjs';
|
|
17
|
+
import { startWith, switchMap, tap, takeUntil } from 'rxjs/operators';
|
|
18
|
+
|
|
19
|
+
import { LocationPickerService } from '../location-picker.service';
|
|
20
|
+
import {
|
|
21
|
+
LocationOption,
|
|
22
|
+
CountryChangeEvent,
|
|
23
|
+
NameChangeEvent,
|
|
24
|
+
CityChangeEvent,
|
|
25
|
+
SelectProps,
|
|
26
|
+
CountryKey,
|
|
27
|
+
} from '../location-picker.types';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Location picker cascade component for PrimeNG v18+.
|
|
31
|
+
*
|
|
32
|
+
* Uses `<p-select>` from `primeng/select` — required for PrimeNG v20 which
|
|
33
|
+
* removed the old `<p-dropdown>` / `primeng/dropdown`.
|
|
34
|
+
* For PrimeNG v17–v19, use `LocationPickerComponent` from `../dropdown` instead.
|
|
35
|
+
*/
|
|
36
|
+
@Component({
|
|
37
|
+
selector: 'cdf-location-picker-select',
|
|
38
|
+
standalone: true,
|
|
39
|
+
imports: [CommonModule, ReactiveFormsModule, SelectModule],
|
|
40
|
+
templateUrl: './location-picker.component.html',
|
|
41
|
+
styleUrls: ['../location-picker.component.css'],
|
|
42
|
+
})
|
|
43
|
+
export class LocationPickerSelectComponent implements OnInit, OnDestroy {
|
|
44
|
+
// ── External reactive-form controls (all optional) ──────────────────────────
|
|
45
|
+
@Input() countryControl?: AbstractControl;
|
|
46
|
+
@Input() provinceControl?: AbstractControl;
|
|
47
|
+
@Input() districtControl?: AbstractControl;
|
|
48
|
+
@Input() cityControl?: AbstractControl;
|
|
49
|
+
|
|
50
|
+
// ── PrimeNG prop passthrough bags ────────────────────────────────────────────
|
|
51
|
+
@Input() countrySelectProps: SelectProps = {};
|
|
52
|
+
@Input() provinceSelectProps: SelectProps = {};
|
|
53
|
+
@Input() districtSelectProps: SelectProps = {};
|
|
54
|
+
@Input() citySelectProps: SelectProps = {};
|
|
55
|
+
|
|
56
|
+
// ── Country option key selectors ─────────────────────────────────────────────
|
|
57
|
+
/** Which field of the country record to use as the dropdown label. Default: 'name' */
|
|
58
|
+
@Input() countryLabelKey: CountryKey = 'name';
|
|
59
|
+
/** Which field of the country record to use as the option value. Default: 'code' */
|
|
60
|
+
@Input() countryValueKey: CountryKey = 'code';
|
|
61
|
+
|
|
62
|
+
// ── Layout ───────────────────────────────────────────────────────────────────
|
|
63
|
+
@Input() layout: 'horizontal' | 'vertical' = 'vertical';
|
|
64
|
+
|
|
65
|
+
// ── Event outputs (for users not using reactive forms) ───────────────────────
|
|
66
|
+
@Output() countryChange = new EventEmitter<CountryChangeEvent>();
|
|
67
|
+
@Output() provinceChange = new EventEmitter<NameChangeEvent>();
|
|
68
|
+
@Output() districtChange = new EventEmitter<NameChangeEvent>();
|
|
69
|
+
@Output() cityChange = new EventEmitter<CityChangeEvent>();
|
|
70
|
+
|
|
71
|
+
// ── Internal fallback controls ───────────────────────────────────────────────
|
|
72
|
+
private _internalCountry = new FormControl<string | null>(null);
|
|
73
|
+
private _internalProvince = new FormControl<string | null>(null);
|
|
74
|
+
private _internalDistrict = new FormControl<string | null>(null);
|
|
75
|
+
private _internalCity = new FormControl<string | null>(null);
|
|
76
|
+
|
|
77
|
+
get _country() { return (this.countryControl ?? this._internalCountry) as FormControl; }
|
|
78
|
+
get _province() { return (this.provinceControl ?? this._internalProvince) as FormControl; }
|
|
79
|
+
get _district() { return (this.districtControl ?? this._internalDistrict) as FormControl; }
|
|
80
|
+
get _city() { return (this.cityControl ?? this._internalCity) as FormControl; }
|
|
81
|
+
|
|
82
|
+
// ── Visibility flags ─────────────────────────────────────────────────────────
|
|
83
|
+
showProvince = false;
|
|
84
|
+
showDistrict = false;
|
|
85
|
+
showCity = false;
|
|
86
|
+
|
|
87
|
+
// ── Dynamic placeholder text (uses selected country's labels) ────────────────
|
|
88
|
+
_provincePlaceholder = 'Select Province';
|
|
89
|
+
_districtPlaceholder = 'Select District';
|
|
90
|
+
|
|
91
|
+
// ── Option streams ───────────────────────────────────────────────────────────
|
|
92
|
+
countryOptions$!: Observable<LocationOption[]>;
|
|
93
|
+
provinceOptions$!: Observable<LocationOption[]>;
|
|
94
|
+
districtOptions$!: Observable<LocationOption[]>;
|
|
95
|
+
cityOptions$!: Observable<LocationOption[]>;
|
|
96
|
+
|
|
97
|
+
private destroy$ = new Subject<void>();
|
|
98
|
+
|
|
99
|
+
constructor(private svc: LocationPickerService) {}
|
|
100
|
+
|
|
101
|
+
ngOnInit(): void {
|
|
102
|
+
this.showCity = !!this.cityControl;
|
|
103
|
+
this.showDistrict = !!this.districtControl || this.showCity;
|
|
104
|
+
this.showProvince = !!this.provinceControl || this.showDistrict;
|
|
105
|
+
|
|
106
|
+
const allCountries = this.svc.getCountries()
|
|
107
|
+
.map(c => ({
|
|
108
|
+
label: (c as any)[this.countryLabelKey] as string,
|
|
109
|
+
value: (c as any)[this.countryValueKey] as string,
|
|
110
|
+
}))
|
|
111
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
112
|
+
this.countryOptions$ = of(allCountries);
|
|
113
|
+
|
|
114
|
+
const mappedValueToCode = new Map<string, string>(
|
|
115
|
+
this.svc.getCountries().map(c => [
|
|
116
|
+
(c as any)[this.countryValueKey] as string,
|
|
117
|
+
c.code,
|
|
118
|
+
])
|
|
119
|
+
);
|
|
120
|
+
const codeToMappedValue = new Map<string, string>(
|
|
121
|
+
this.svc.getCountries().map(c => [
|
|
122
|
+
c.code,
|
|
123
|
+
(c as any)[this.countryValueKey] as string,
|
|
124
|
+
])
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const rawCode = (val: string | null): string | null =>
|
|
128
|
+
val ? (mappedValueToCode.get(val) ?? val) : null;
|
|
129
|
+
|
|
130
|
+
this.provinceOptions$ = this._country.valueChanges.pipe(
|
|
131
|
+
takeUntil(this.destroy$),
|
|
132
|
+
startWith(this._country.value),
|
|
133
|
+
tap((val: string | null) => {
|
|
134
|
+
this._province.setValue(null, { emitEvent: false });
|
|
135
|
+
this._district.setValue(null, { emitEvent: false });
|
|
136
|
+
this._city.setValue(null, { emitEvent: false });
|
|
137
|
+
const code = rawCode(val);
|
|
138
|
+
if (code) {
|
|
139
|
+
const country = this.svc.getCountries().find(c => c.code === code);
|
|
140
|
+
if (country) {
|
|
141
|
+
this._provincePlaceholder = `Select ${country.provinceLabel ?? 'Province'}`;
|
|
142
|
+
this._districtPlaceholder = `Select ${country.districtLabel ?? 'District'}`;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
this._provincePlaceholder = 'Select Province';
|
|
146
|
+
this._districtPlaceholder = 'Select District';
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
switchMap((val: string | null) => {
|
|
150
|
+
const code = rawCode(val);
|
|
151
|
+
return code
|
|
152
|
+
? of(this.svc.getProvinces(code).map(p => ({ label: p, value: p })))
|
|
153
|
+
: of([]);
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
this.districtOptions$ = this._province.valueChanges.pipe(
|
|
158
|
+
takeUntil(this.destroy$),
|
|
159
|
+
startWith(this._province.value),
|
|
160
|
+
tap((name: string | null) => {
|
|
161
|
+
this._district.setValue(null, { emitEvent: false });
|
|
162
|
+
this._city.setValue(null, { emitEvent: false });
|
|
163
|
+
if (name && !this._country.value) {
|
|
164
|
+
const code = this.svc.getCountryByProvince(name);
|
|
165
|
+
if (code) {
|
|
166
|
+
this._country.setValue(codeToMappedValue.get(code) ?? code);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
switchMap((name: string | null) => {
|
|
171
|
+
const code = rawCode(this._country.value);
|
|
172
|
+
return name && code
|
|
173
|
+
? of(this.svc.getDistricts(code, name).map(d => ({ label: d, value: d })))
|
|
174
|
+
: of([]);
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
this.cityOptions$ = this._district.valueChanges.pipe(
|
|
179
|
+
takeUntil(this.destroy$),
|
|
180
|
+
startWith(this._district.value),
|
|
181
|
+
tap(() => this._city.setValue(null, { emitEvent: false })),
|
|
182
|
+
switchMap((dist: string | null) => {
|
|
183
|
+
const code = rawCode(this._country.value);
|
|
184
|
+
const prov = this._province.value;
|
|
185
|
+
return dist && code && prov
|
|
186
|
+
? of(this.svc.getCities(code, prov, dist).map(c => ({ label: c, value: c })))
|
|
187
|
+
: of([]);
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
this._country.valueChanges
|
|
192
|
+
.pipe(takeUntil(this.destroy$))
|
|
193
|
+
.subscribe((v: string | null) => {
|
|
194
|
+
const code = rawCode(v);
|
|
195
|
+
const country = code ? (this.svc.getCountries().find(c => c.code === code) ?? null) : null;
|
|
196
|
+
this.countryChange.emit({ formValue: v, country });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this._province.valueChanges
|
|
200
|
+
.pipe(takeUntil(this.destroy$))
|
|
201
|
+
.subscribe((v: string | null) => this.provinceChange.emit({ name: v }));
|
|
202
|
+
|
|
203
|
+
this._district.valueChanges
|
|
204
|
+
.pipe(takeUntil(this.destroy$))
|
|
205
|
+
.subscribe((v: string | null) => this.districtChange.emit({ name: v }));
|
|
206
|
+
|
|
207
|
+
this._city.valueChanges
|
|
208
|
+
.pipe(takeUntil(this.destroy$))
|
|
209
|
+
.subscribe((v: string | null) => {
|
|
210
|
+
const code = rawCode(this._country.value);
|
|
211
|
+
const prov = this._province.value;
|
|
212
|
+
const dist = this._district.value;
|
|
213
|
+
const raw = (code && prov && dist && v)
|
|
214
|
+
? this.svc.getCityRaw(code, prov, dist, v)
|
|
215
|
+
: null;
|
|
216
|
+
this.cityChange.emit({ name: v, postalCode: raw?.postalCode ?? '', type: raw?.type });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ngOnDestroy(): void {
|
|
221
|
+
this.destroy$.next();
|
|
222
|
+
this.destroy$.complete();
|
|
223
|
+
}
|
|
224
|
+
}
|
/package/src/angular/{location-picker.component.html → dropdown/location-picker.component.html}
RENAMED
|
File without changes
|