3

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.

  1. 'Select country/state/city' like default text , it shows in dropdown not in inputbox enter image description here
  2. I am not able to show fetched data properly. its showing like below enter image description here

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

country data

state data response - gets on country select (I have selected country id =1) enter image description here

city data response - get on state select (I have selected state id =42) enter image description here

ganesh
  • 416
  • 1
  • 11
  • 32

3 Answers3

1

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

  1. The placeholder messages when appropriate.
  2. The already selected value. Please see 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()]
    });
Teodor
  • 296
  • 1
  • 6
  • Thanks for helping me. In my case, country, state, city are required. and I am patching database values in ngOnit. I am loading state , city on click event. So I don't have static arrays of state,city in which I can pass database IDs. – ganesh Nov 30 '21 at 15:29
  • I have changed the code example to include also the load from server scenario. Please let me know if this helps. – Teodor Nov 30 '21 at 16:59
  • tried your logic of data binding and then patching in ngOnit but still its showing ids in select box rather than name :-( – ganesh Dec 01 '21 at 18:55
  • can you update the question with the example you are having now? And please include a data structure example. – Teodor Dec 01 '21 at 19:12
  • pls check Edit in my question. In edit, I tried your logic for placeholder and for data fetching. But its getting more confusing to me as required not works because of default messages for controls. Its getting difficult to achieve required validation, placeholder(for first time if database has null values, before user update), showing database names on select (after user update) – ganesh Dec 01 '21 at 19:32
  • pls correct and guide me if I am applying your logic in wrong way. I have only one form in which user will add details for first time and later added details will gets populates on same form. Then user can update details from same form. – ganesh Dec 01 '21 at 19:41
  • The logic you implemented seems fine to me. So a problem can be in the data. Can you please show a partial response for `this.cscService.getCountries()` and `this.cscService.getStates()`? I am interested in what `response.data` contains. Please see my edit for form validation. – Teodor Dec 01 '21 at 22:31
  • I have added response data. please check. – ganesh Dec 02 '21 at 10:29
  • Hi ganesh. Unfortunately I cannot spot the problem you are having. Last try I can think of is to add a delay to the patching: `setTimeout(() => { this.guestForm.patchValue({ country: this.dbCountryId; }); }, 500)`. If this changes anything, let me know. – Teodor Dec 02 '21 at 17:02
  • timeout not changing anything. Thanks for your help and guidance. upvoted for your efforts. thank you – ganesh Dec 02 '21 at 17:42
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/239790/discussion-between-teodor-and-ganesh). – Teodor Dec 02 '21 at 18:57
0

This solution seems to work. You need formControl functionality in the ngOnInit function of the controller. Hope this helps.

In the html file, I have:

<div class="position">
  <form [formGroup]="FormOne">
    <ng-select
      [formControlName]="city"
      (change)="onChangeCity($event)"
      placeholder="Select City"
      ><ng-option *ngFor="let city of cities" [value]="city.id">{{
        city.name
      }}</ng-option>
    </ng-select>
  </form>
 </div>

in the Controller I have this for example sake:

import { Component, OnInit } from '@angular/core';
import {
  FormGroup,
  Validators,
  FormControl,
  AbstractControl,
} from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  cities = [
    { id: 1, name: 'abc' },
    { id: 2, name: 'def' },
    { id: 3, name: 'ghi' },
    { id: 4, name: 'jkl' },
    { id: 5, name: 'mno' },
  ];
  FormOne: FormGroup;

  constructor() {}

  ngOnInit() {
    this.FormOne = new FormGroup({
      city: new FormControl(null, Validators.required),
    });
  }
 
}
Mana S
  • 509
  • 2
  • 6
0

you should try items instate of option as bellow

<ng-container ngFor="let country in countries"> \\ you can apply forloop like this and it will show default value
    <ng-select
      [items]="{{country.name}}"
      formControlName="country"
      bindLabel="label"                 
      [multiple]="true"
      placeholder="Select Country"
      [(ngModel)]="selectedAttributes">
    </ng-select>
</ng-container>

add following modules

import { NgSelectModule } from '@ng-select/ng-select';
import { FormsModule } from '@angular/forms';


@NgModule({
  imports: [
    FormsModule,
    NgSelectModule,
 

for more information you can checkout this link ng-select docs

Vishal Pandey
  • 349
  • 1
  • 4
  • 15