I have 3 dropdowns for country,state,city in angular. I have used ng-select module for those dowpdowns from reference here. On country change states populates, and on state change city populate.
template HTML
<ng-select formControlName="country" (change)="onChangeCountry($event)" >
<ng-option value="dbCountryId ? dbCountryId : ''">{{dbCountryName ? dbCountryName : 'Select Country' }}</ng-option>
<ng-option *ngFor="let country of countryInfo" [value]="country.id">{{country.name}}</ng-option>
</ng-select>
<ng-select formControlName="state" (change)="onChangeState($event)">
<ng-option value="dbStateId ? dbStateId : ''">{{dbStateName ? dbStateName : 'Select State' }}</ng-option>
<ng-option *ngFor="let state of stateInfo" [value]="state.id">{{state.name}}</ng-option>
</ng-select>
<ng-select formControlName="city" >
<ng-option value="dbCityId ? dbCityId : ''">{{dbCityName ? dbCityName : 'Select City' }}</ng-option>
<ng-option *ngFor="let city of cityInfo" [value]="city.id">{{city.name}}</ng-option>
</ng-select>
ts code
this.userService.getUserDetails(userDetails.id).subscribe((results) => {
if (results['status'] === true) {
this.dbCountryName = results.data.country ? results.data.country : null;
this.dbCountryId = results.data.country_id
? results.data.country_id
: null;
this.dbStateName = results.data.state ? results.data.state : null;
this.dbStateId = results.data.state_id
? results.data.state_id
: null;
this.dbCityName = results.data.city ? results.data.city : null;
this.dbCityId = results.data.city_id ? results.data.city_id : null;
this.form.patchValue({
country:
results.data.country_id === null ? '' : results.data.country_id,
state:
results.data.state_id === null ? '' : results.data.state_id,
city: results.data.city_id === null ? '' : results.data.city_id,
});
}
});
I am using same form for add and edit data. I am storing id of country,state, city. In api response I get stored id, name of fields. I have patched id with respective form control.
I have 2 problems.
How I can solve these problems with ng-select in angular? please help and guide. Thanks.
Edit
Template code
<div class="col-sm-6">
<div class="form-group">
<label for="country">Country <b style="color: red">*</b></label><ng-select formControlName="country" (change)="onChangeCountry($event)" [ngClass]="{ 'error_border': submitted && f.country.errors }">
<ng-option *ngFor="let country of countryInfo" [value]="country.id">{{country.name}}</ng-option>
</ng-select>
<div *ngIf="submitted && f.country.errors" class="text-danger">
<div *ngIf="f.country.errors.required">Country is required</div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="state">State <b style="color: red">*</b></label>
<ng-select formControlName="state" [ngClass]="{ 'error_border': submitted && f.state.errors }" (change)="onChangeState($event)">
<ng-option *ngFor="let state of stateInfo" [value]="state.id">{{state.name}}</ng-option>
</ng-select>
<div *ngIf="submitted && f.state.errors" class="text-danger">
<div *ngIf="f.state.errors.required">State is required</div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="city">City <b style="color: red">*</b></label>
<ng-select formControlName="city" [ngClass]="{ 'error_border': submitted && f.city.errors }">
<ng-option *ngFor="let city of cityInfo" [value]="city.id">{{city.name}}</ng-option>
</ng-select>
<div *ngIf="submitted && f.city.errors" class="text-danger">
<div *ngIf="f.city.errors.required">City is required</div>
</div>
</div>
</div>
ts code
export class EditProfileComponent implements OnInit {
stateInfo: any[] = [];
countryInfo: any[] = [];
cityInfo: any[] = [];
dbCountryName = '';
dbCountryId = 0;
dbStateName = '';
dbStateId = 0;
dbCityName = '';
dbCityId = 0;
ngOnInit() {
this.form = this.formBuilder.group({
country: ['Select Country', Validators.required],
state: ['Select State', Validators.required],
city: ['Select City', Validators.required],
});
this.userService.getUserDetails(userDetails.id).subscribe((results) => {
if (results['status'] === true) {
this.dbStateName = results.data.state ? results.data.state : null;
this.dbStateId = results.data.state_id
? results.data.state_id
: null;
this.dbCityName = results.data.city ? results.data.city : null;
this.dbCityId = results.data.city_id ? results.data.city_id : null;
this.dbCountryName = results.data.country ? results.data.country : null;
this.dbCountryId = results.data.country_id
? results.data.country_id
: null;
this.cscService.getCountries().subscribe((result) => {
this.countryInfo = result.data;
this.form.patchValue({
country: this.dbCountryId
});
});
this.cscService.getStates(this.dbCountryId).subscribe((result) => {
this.stateInfo = result.data;
this.form.patchValue({
state: this.dbStateId
});
});
this.cscService
.getCities(this.dbStateId)
.subscribe((result) => {
this.cityInfo = result.data;
this.form.patchValue({
city: this.dbCityId
});
}
);
this.form.patchValue({
// country:
// results.data.country_id === null ? 'Select Country' : results.data.country_id,
// state:
// results.data.state_id === null ? 'Select State' : results.data.state_id,
// city: results.data.city_id === null ? 'Select City' : results.data.city_id,
});
}
});
}
getCountries() {
this.cscService.getCountries().subscribe((result) => {
this.countryInfo = result.data;
});
}
onChangeCountry(countryId: number) {
if (countryId) {
this.cscService.getStates(countryId).subscribe((result) => {
this.stateInfo = result.data;
this.cityInfo = null;
});
this.form.patchValue({
state: "Select State",
city: "Select City"
});
} else {
this.stateInfo = null;
this.cityInfo = null;
}
}
onChangeState(stateId: number) {
if (stateId) {
this.cscService
.getCities(stateId)
.subscribe((result) => (this.cityInfo = result.data));
this.form.patchValue({ city: "Select City" });
} else {
this.cityInfo = null;
}
}
}
country data response
state data response - gets on country select (I have selected country id =1)
city data response - get on state select (I have selected state id =42)
From what I understand you are using Reactive Forms to manage these controls. If this is not true, please let me know.
The HTML template I am suggesting is similar to yours, but simpler. I am recommending to not add a separate <ng-option>
for the selected value / default message:
<ng-select formControlName="country" (change)="onChangeCountry($event)" style="width: 200px;">
<ng-option *ngFor="let country of countries" [value]="country.id">{{country.name}}</ng-option>
</ng-select>
<ng-select formControlName="state" (change)="onChangeState($event)" style="width: 200px;">
<ng-option *ngFor="let state of statesToShow" [value]="state.id">{{state.name}}</ng-option>
</ng-select>
<ng-select formControlName="city" (change)="onChangeCity($event)" style="width: 200px;">
<ng-option *ngFor="let city of citiesToShow" [value]="city.id">{{city.name}}</ng-option>
</ng-select>
I have also included a TS file with working example on how to set
prefefinedValues()
function.Please note that in order for 2. to work as expected, the ID of the element needs to be in the data source currently selected for the control (in my example statesToShow
or citiesToShow
). If not, it will be displayed as text (probably what you are experiencing).
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'test2';
guestForm: FormGroup;
selectedCar: number = 0;
// this is the data source for the STATES drop-down (initially empty)
// => will be populated when a COUNTRY is selected
public statesToShow: Array<any> = [];
// this is the data source for the CITIES drop-down (initially empty)
// => will be populated when a STATE is selected
public citiesToShow: Array<any> = [];
// TEST data start
public countries = [
{ id: 1, name: 'Romania' },
{ id: 2, name: 'USA' },
{ id: 3, name: 'France' },
{ id: 4, name: 'Spain' },
];
public states = [
[],
[
{id: 1, name: "Cluj"},
{id: 2, name: "Valcea"},
{id: 3, name: "Sibiu"},
{id: 4, name: "Mures"},
],
[
{id: 5, name: "New York"},
{id: 6, name: "Oregon"},
{id: 7, name: "Arizona"},
{id: 8, name: "Texas"},
],
[
{id: 9, name: "Normandie"},
{id: 10, name: "Ile-de-France"},
{id: 11, name: "Grand Est"},
{id: 12, name: "Occitanie"},
],
[
{id: 13, name: "Alicante"},
{id: 14, name: "Valencia"},
{id: 15, name: "Sevilla"},
{id: 16, name: "Malaga"},
]
];
public cities = [
[],
[
{id: 1, name: "Cluj-Napoca"},
{id: 2, name: "Turda"},
{id: 3, name: "Huedin"},
],
[
{id: 4, name: "Ramnicul Valcea"},
{id: 5, name: "Horezu"},
{id: 6, name: "Olanesti"},
],
[],
[],
[
{id: 10, name: "New York city 1"},
{id: 11, name: "New York city 2"},
{id: 12, name: "New York city 3"},
],
[
{id: 13, name: "Oregon city 1"},
{id: 14, name: "Oregon city 2"},
{id: 15, name: "Oregon city 3"},
]
]
// TEST data end
private dbCountryId: number | null = null;
private dbStateId: number | null = null;
private dbCityId: number | null = null;
constructor(private _fb: FormBuilder) {
// add default placeholder messages for all the controls
this.guestForm = this._fb.group({
country: ['Please select country', null],
state: ['Please select state', null],
city: ['Please select city', null]
});
}
ngOnInit() {
}
onChangeCountry(value: number) {
// set the data source for the STATES drop-down
this.statesToShow = this.states[value];
// display placeholders for STATES and CITIES until the user
// selects the values
this.guestForm.patchValue({
state: "Please select state !",
city: "Please select city !"
});
}
onChangeState(value: number) {
// set the data source for the CITIES drop-down
this.citiesToShow = this.cities[value] ? this.cities[value] : [];
// display the placeholder until the user selects a new city
this.guestForm.patchValue({ city: "Please select city !" });
}
onChangeCity(value: number) {
console.log(value);
}
// example on how to correctly set preselected values
// in the controls
predefinedValues() {
// preselected values (MUST BE A VALID COMBINATION)
this.dbCountryId = 2;
this.dbStateId = 6;
this.dbCityId = 14;
// set the sources for STATES and CITIES drop-downs
this.statesToShow = this.states[this.dbCountryId];
this.citiesToShow = this.cities[this.dbStateId];
// set the preselected IDs as current value in all drop-downs
this.guestForm.patchValue({
country: this.dbCountryId,
state: this.dbStateId,
city: this.dbCityId
});
}
}
EDIT: loading data from a server
When the data is received from a server, we need to wait for the information to arrive before making any patching to the form control. The predefinedValues
function changes to:
predefinedValues() {
// read saved database values:
this._dataService.getSavedDatabaseValues()
.then(data => {
this.dbCountryId = data.dbCountryId;
this.dbStateId = data.dbStateId;
this.dbCityId = data.dbCityId;
// now that we have the saved IDs,
// load countries, states & cities for the saved data
this._dataService.getCountries()
.then(data => {
this.countriesToShow = data;
// now that the data binding to the control is complete
// we can do the patching
this.guestForm.patchValue({
country: this.dbCountryId
});
});
this._dataService.getStates(this.dbCountryId)
.then(data => {
this.statesToShow = data;
// now that the data binding to the control is complete
// we can do the patching
this.guestForm.patchValue({
state: this.dbStateId
});
});
this._dataService.getCities(this.dbStateId)
.then(data => {
this.citiesToShow = data;
// now that the data binding to the control is complete
// we can do the patching
this.guestForm.patchValue({
city: this.dbCityId
});
});
})
}
This function can be called directly in ngOnInit
to load the previously saved data. Also, when one of the countries
or states
selections change, we need to load the data from the server.
onChangeCountry(value: number) {
// set the data source for the STATES drop-down
this._dataService.getStates(value)
.then(data => {
this.statesToShow = data;
});
// display placeholders for STATES and CITIES until the user
// selects the values
this.guestForm.patchValue({
state: "Please select state !",
city: "Please select city !"
});
}
Edit 2: In order to fix the issue with the required
validation, I am suggesting a custom validator:
import {AbstractControl, ValidatorFn} from '@angular/forms';
export function selectIsRequired(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
console.log("validator:", control.value);
return control.value === 'Select Country'
|| control.value === 'Select State'
|| control.value === 'Select City'
|| isNaN(parseInt(control.value))
? {required: control.value} : null;
}
}
This will be applied on the select
controls like this:
this.guestForm = this._fb.group({
country: ['Select country', selectIsRequired()],
state: ['Select state', selectIsRequired()],
city: ['Select city', selectIsRequired()]
});