angularformsangular-reactive-formsformgroupsform-control

how to use child components with parent component - angular


I created this component called country-flag-dropdown.ts and html Component:

import {Component, OnInit, Input, Output, EventEmitter} from "@angular/core";
import {FormGroup, FormControl} from "@angular/forms";
import {CountryDataService} from "projects/pitxpress-admin-app/src/app/pitxpress/api-services/country/country-data.service";
import {ICountry} from "projects/pitxpress-admin-app/src/app/pitxpress/api-services/country/models/country.interface";
import {PhoneNumberUtil, PhoneNumberFormat} from "google-libphonenumber";

/**
 * Country flag dropdown component.
 */
@Component({
    selector: "app-country-flag-dropdown",
    templateUrl: "./country-flag-dropdown.component.html",
    styleUrls: ["./country-flag-dropdown.component.scss"],
})
export class CountryFlagDropdownComponent implements OnInit {
    /**
     * Form group for the component.
     */
    @Input() public form!: FormGroup;

    /**
     * Control name for the phone number input.
     */
    @Input() public controlName!: string;

    /**
     * Event emitter for phone number changes.
     */
    @Output() public phoneNumberChange = new EventEmitter<string>();

    /**
     * List of countries.
     */
    public countries: ICountry[] = [];

    /**
     * Selected dial code.
     */
    public selectedDialCode: string = "";

    /**
     * Selected country.
     */
    public selectedCountry: ICountry | undefined;

    /**
     * Country control form control.
     */
    public countryControl = new FormControl();

    /**
     * Phone number utility instance.
     */
    private phoneNumberUtil = PhoneNumberUtil.getInstance();

    /**
     * Form group for country control.
     */
    public countryForm = new FormGroup({
        countryControl: new FormControl(),
    });

    /**
     * Constructor.
     * @param countryDataService Country data service instance.
     */
    constructor(private countryDataService: CountryDataService) {}

    /**
     * Initialize component.
     */
    public ngOnInit(): void {
        // Initialize countries data and set initial country to United States if it exists
        this.countryDataService.getCountries().subscribe((countries) => {
            this.countries = countries;
            this.initInitialCountry();
            this.checkExistingPhoneNumber();
        });

        // Subscribe to country control value changes
        this.countryControl.valueChanges.subscribe((value) => {
            this.onCountryChange(value);
        });
    }

    /**
     * Initialize initial country to United States if it exists.
     */
    private initInitialCountry(): void {
        const usa = this.countries.find((country) => country.isoCode2 === "US");
        if (usa) {
            this.selectedCountry = usa;
            this.selectedDialCode = usa.dialCodes[0];
            this.countryControl.setValue(this.selectedDialCode, {emitEvent: false});
            this.form.get(this.controlName)?.setValue(this.selectedDialCode);
        }
    }

    /**
     * Check if there's an existing phone number.
     */
    private checkExistingPhoneNumber(): void {
        const phoneNumber = this.form.get(this.controlName)?.value;
        if (phoneNumber) {
            this.selectedDialCode = this.extractDialCode(phoneNumber);
            this.form.get(this.controlName)?.setValue(phoneNumber);
            const iso2Code = this.getIso2CodeFromPhoneNumber(phoneNumber);
            if (iso2Code) {
                this.updateCountrySelectionByIsoCode(iso2Code);
            }
        }
    }

    /**
     * Handle country change event.
     * @param dialCode Selected dial code.
     */
    public onCountryChange(dialCode: string): void {
        const selectedCountry = this.countries.find((country) => country.dialCodes.includes(dialCode));
        if (selectedCountry) {
            this.selectedDialCode = dialCode;
            this.selectedCountry = selectedCountry;
            const inputValue = this.form.get(this.controlName)?.value.replace(this.selectedDialCode, "");
            this.form.get(this.controlName)?.setValue(this.selectedDialCode + inputValue);
            this.phoneNumberChange.emit(this.selectedDialCode + inputValue);
        } else {
            // If the country is selected from the dropdown, update the selectedCountry and selectedDialCode
            const selectedCountry = this.countries.find((country) => country.dialCodes[0] === dialCode);
            if (selectedCountry) {
                this.selectedCountry = selectedCountry;
                this.selectedDialCode = dialCode;
            }
        }
    }

    /**
     * Handle phone number input event.
     * @param event Input event.
     */
    public onPhoneNumberInput(event: any): void {
        const inputElement = event.target as HTMLInputElement;
        const inputValue = inputElement.value.trim();
        this.formatPhoneNumber(inputValue);
        const countryCode = this.getCountryCodeFromPhoneNumber(inputValue);
        if (countryCode) {
            const isoCode = this.phoneNumberUtil.getRegionCodeForCountryCode(parseInt(countryCode));
            if (isoCode) {
                this.updateCountrySelectionByIsoCode(isoCode);
            }
        }
        // Update the phone number value in the form
        this.form.get(this.controlName)?.setValue(inputValue);
        // Emit the phone number change event
        this.phoneNumberChange.emit(inputValue);
    }

    /**
     * Extract dial code from phone number.
     * @param phoneNumber Phone number.
     * @returns Dial code.
     */
    private extractDialCode(phoneNumber: string): string {
        for (const country of this.countries) {
            if (Array.isArray(country.dialCodes)) {
                for (const dialCode of country.dialCodes) {
                    if (phoneNumber.startsWith(dialCode)) {
                        return dialCode;
                    }
                }
            }
        }
        return "";
    }

    /**
     * Update country selection by ISO code.
     * @param isoCode ISO code.
     */
    private updateCountrySelectionByIsoCode(isoCode: string): void {
        const selectedCountry = this.countries.find((country) => country.slug === isoCode);
        if (selectedCountry) {
            console.log("Updating country selection by ISO code:", selectedCountry);
            this.selectedCountry = selectedCountry;
            this.selectedDialCode = selectedCountry.dialCodes[0];
            this.countryControl.setValue(this.selectedDialCode, {emitEvent: false});
        }
    }

    /**
     * Get ISO 2 code from phone number.
     * @param phoneNumber Phone number.
     * @returns ISO 2 code.
     */
    private getIso2CodeFromPhoneNumber(phoneNumber: string): string {
        try {
            const number = this.phoneNumberUtil.parse(phoneNumber, ""); // Use the default region for parsing
            return this.phoneNumberUtil.getRegionCodeForNumber(number) || "";
        } catch (error) {
            console.error("Error parsing phone number:", error);
            return "";
        }
    }

    /**
     * Get country code from phone number.
     * @param phoneNumber Phone number.
     * @returns Country code.
     */
    private getCountryCodeFromPhoneNumber(phoneNumber: string): string {
        try {
            const number = this.phoneNumberUtil.parse(phoneNumber, "");
            return number?.getCountryCode()?.toString() || "";
        } catch (error) {
            console.error("Error parsing phone number:", error);
            return "";
        }
    }

    /**
     * Format phone number.
     * @param phoneNumber Phone number.
     * @returns Formatted phone number.
     */
    public formatPhoneNumber(phoneNumber: string): string {
        try {
            const number = this.phoneNumberUtil.parse(phoneNumber, this.selectedCountry?.isoCode2);
            return this.phoneNumberUtil.format(number, PhoneNumberFormat.INTERNATIONAL);
        } catch (error) {
            console.error("Error formatting phone number:", error);
            return phoneNumber;
        }
    }
}

HTML:

<div class="input-group country-dropdown-group" [formGroup]="form">
    <form [formGroup]="countryForm">
    <p-dropdown 
        [options]="countries" 
        optionLabel="name" 
        optionValue="dialCodes[0]" 
        appendTo="body"
        [filter]="true" 
        filterBy="name"
        formControlName="countryControl"
        class="p-inputgroup-addon country-flag-selector">
  
        <ng-template let-country pTemplate="selectedItem">
            <div class="country-selector-item">
        <img [src]="'assets/flags/' + selectedCountry?.slug + '.svg'" class="country-flag" />
                <span>{{ selectedCountry?.slug }}</span>
            </div>
        </ng-template>
  
        <ng-template let-country pTemplate="item">
            <div class="country-selector-item">
                <img [src]="'assets/flags/' + country.slug + '.svg'" class="country-flag" />
                <span>{{ country.name }}</span>
            </div>
        </ng-template>
    </p-dropdown>
    </form>
    <input 
        type="text" 
        pInputText 
        [formControlName]="controlName" 
        maxlength="15" 
        placeholder="Phone Number" 
        class="form-control" 
        (input)="onPhoneNumberInput($event)" 
        [value]="formatPhoneNumber(form.get(controlName)?.value)"/>        
</div>

My goal is creating a country flag dropdown where the user can select flag or type in a phone number and the flag will show. But with this current code I'm not able to pick a flag from the dropdown and it won't reflect in the UI.

I think I have a general idea on where it's breaking. I have an @Input Form but I also have a CountyForm. Both forms seem to be being accessed in different places which is just getting a bit confusing. The idea of the @Input form - is this to try to add to an existing form on the parent component. In this line <img [src]="'assets/flags/' + selectedCountry?.slug + '.svg'" class="country-flag" /> if I change the selectedCountry?.slug to country, I'm able to pick the flag from the dropdown and it will relflect on the UI but then I'm not able to detect a flag when I type in a number so its taking away one fuctionality when I need both. I'm just curious as to how I can accomplish this?


Solution

  • You can try adding in your CountryFlagDropdownComponent OnInit this subscription:

    this.form.get(this.controlName)?.valueChanges.subscribe((value) => {
       this.onPhoneNumberInput(value);
    });
    

    The onPhoneNumberInput method takes a phoneNumber as input and updates the selected country and onCountryChange method updates the phone number based on selected country from the dropdown.

    public onPhoneNumberInput(phoneNumber: string): void {
        this.formatPhoneNumber(phoneNumber);
        const countryCode = this.getCountryCodeFromPhoneNumber(phoneNumber);
        if (countryCode) {
          const isoCode = this.phoneNumberUtil.getRegionCodeForCountryCode(parseInt(countryCode));
          if (isoCode) {
            this.updateCountrySelectionByIsoCode(isoCode);
          }
        }
        this.form.get(this.controlName)?.setValue(phoneNumber);
        this.phoneNumberChange.emit(phoneNumber);
      }
    public onCountryChange(dialCode: string): void {
        const selectedCountry = this.countries.find((country) => country.dialCodes.includes(dialCode));
        if (selectedCountry) {
          this.selectedDialCode = dialCode;
          this.selectedCountry = selectedCountry;
          const inputValue = this.form.get(this.controlName)?.value.replace(this.selectedDialCode, "");
          this.form.get(this.controlName)?.setValue(this.selectedDialCode + inputValue);
          this.phoneNumberChange.emit(this.selectedDialCode + inputValue);
        }
      }
    

    At the end you should update the html with (input)="onPhoneNumberInput($event.target.value)"