Skip to main content
cngx-src documentation

CngxPhoneInput

ComponentPrimaryOnPushv0.1.0WCAG AA

projects/forms/input/phone-input/phone-input.component.ts

Import#

import { CngxPhoneInput } from '@cngx/forms/input'

Description#

International phone field composing a country picker with a region-aware mask.

CngxPhoneInput wires a CngxSelect (country) to a CngxInputMask (phone:<region>): the selected country drives the mask region through a computed(), so picking a country re-targets the mask with zero manual sync. It provides CNGX_FORM_FIELD_CONTROL and is the single form-field control - the inner select is shielded from the surrounding cngx-form-field (a null CngxFormFieldPresenter in the inner element's providers, via CngxPhoneInputDetach) so only this component wires the field ARIA and value.

The country list is consumer-overridable through [countries]. Selecting a country pre-fills its dial code (e.g. +49); switching country clears the entered national number (the mask's documented auto-clear on pattern change) and re-seeds the new dial code.

<cngx-form-field [field]="f.phone">
  <cngx-phone-input [(value)]="phone" />
</cngx-form-field>

https://cngxjs.github.io/cngx/examples/#/forms/input/phone/intl

Metadata#

Host#

Providers#

CNGX_FORM_FIELD_CONTROL
useExisting CngxPhoneInput

Relationships

Index#

Inputs#

ariaLabel#string
input()

Accessible label used when standalone (no cngx-form-field).

default ''
input()

Overrides the picker's country list.

default CNGX_PHONE_COUNTRIES
country#Country
model()

The selected country. Two-way bindable; defaults to the first entry.

default CNGX_PHONE_COUNTRIES[0]
countryAriaLabel#string
input()

Accessible label for the country picker. Per-instance override; otherwise resolves through CNGX_INPUT_CONFIG.ariaLabels.phoneCountry (EN 'Country').

default ''
disabledInput#boolean
input()

Consumer disable knob; the effective disabled also folds in the field.

default false, { alias: 'disabled' }
disabledReason#string
input()

Reason announced via aria-describedby while the control is disabled.

default ''
lineType#"auto" | "landline" | "mobile"
model()

Which mask alternate to use. 'auto' (default) picks landline vs mobile by length; 'mobile'/'landline' force that grouping immediately, with no length threshold to cross. Two-way bindable for a manual switch.

default 'auto'
value#string
model()

The masked phone number (raw digits the mask accepted). Two-way bindable.

default ''

Outputs#

country#Country
model()

The selected country. Two-way bindable; defaults to the first entry.

lineType#"auto" | "landline" | "mobile"
model()

Which mask alternate to use. 'auto' (default) picks landline vs mobile by length; 'mobile'/'landline' force that grouping immediately, with no length threshold to cross. Two-way bindable for a manual switch.

value#string
model()

The masked phone number (raw digits the mask accepted). Two-way bindable.

Instance Properties#

focused#unknown
Readonly
this.focusedState.asReadonly()

Methods#

focus#void
focus(options?: FocusOptions)
@paramoptions?FocusOptions

HostBindings#

BindingExpression
[attr.aria-labelledby]labelledBy()
[attr.aria-label]ariaLabelAttr()
[attr.aria-disabled]ariaDisabled()
[class.cngx-phone-input--disabled]disabled()
[class.cngx-phone-input--focused]focused()

HostListeners#

EventHandler
(focusin)focusin()
(focusout)focusout()

libphonenumber-js adapter

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { form, schema, required } from '@angular/forms/signals';
import { parsePhoneNumberFromString, type CountryCode } from 'libphonenumber-js';

import { CngxFormField, CngxLabel, CngxHint } from '@cngx/forms/field';
import {
  CngxPhoneInput,
  providePhoneMetadata,
  type CngxPhoneMetadata,
  type Country,
} from '@cngx/forms/input';

/**
 * CngxPhoneInput driven by a real libphonenumber-js line-type adapter.
 *
 * The metadata-adapter demo story ships a toy regex stub; this playground
 * wires the production library instead. The adapter receives the national
 * subscriber digits (CngxPhoneInput strips the dial code) and the region,
 * parses them with libphonenumber-js, and maps the parsed number type onto
 * the strategy contract. As you type, the mask switches to the mobile
 * grouping the moment libphonenumber can classify the prefix - the typed
 * digits stay put across the regroup.
 *
 * libphonenumber-js lives in the playground dependencies only; cngx never
 * adds it to its own graph - the adapter is consumer-authored by design.
 */
const libphonenumberAdapter: CngxPhoneMetadata = {
  lineType: (region, national) => {
    if (!national) {
      return 'unknown';
    }
    const parsed = parsePhoneNumberFromString(national, region as CountryCode);
    switch (parsed?.getType()) {
      case 'MOBILE':
        return 'mobile';
      case 'FIXED_LINE':
        return 'fixedLine';
      default:
        // Ambiguous (FIXED_LINE_OR_MOBILE) or not yet classifiable: keep the
        // length-based fallback rather than guessing.
        return 'unknown';
    }
  },
};

@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [CngxFormField, CngxLabel, CngxHint, CngxPhoneInput],
  viewProviders: [providePhoneMetadata(libphonenumberAdapter)],
  template: `
    <div style="max-width: 22rem; display: grid; gap: 0.75rem">
      <p style="margin: 0; opacity: 0.8; font-size: 0.875rem">
        libphonenumber-js classifies the number as you type. Pick Germany and
        type a number starting 15/16/17, or pick the UK and type one starting
        7 - the grouping switches to mobile and your digits are preserved.
      </p>
      <cngx-form-field [field]="phoneField">
        <label cngxLabel>Phone number</label>
        <cngx-phone-input [countries]="countries" [(country)]="country" />
        <span cngxHint>Line type comes from libphonenumber-js, not a fixed mask</span>
      </cngx-form-field>
      <span style="font-size: 0.8125rem; opacity: 0.7">
        Country: {{ country().label }} &middot; Value: {{ phoneField().value() || '(empty)' }}
      </span>
    </div>
  `,
})
export class LibphonenumberExample {
  protected readonly countries: Country[] = [
    { region: 'DE', dialCode: '+49', label: 'Germany' },
    { region: 'AT', dialCode: '+43', label: 'Austria' },
    { region: 'GB', dialCode: '+44', label: 'United Kingdom' },
    { region: 'US', dialCode: '+1', label: 'United States' },
  ];
  protected readonly country = signal<Country>(this.countries[0]);

  private readonly phoneModel = signal({ phone: '' });
  private readonly phoneSchema = schema<{ phone: string }>((root) => {
    required(root.phone);
  });
  protected readonly phoneForm = form(this.phoneModel, this.phoneSchema);
  protected readonly phoneField = this.phoneForm.phone;
}
export { LibphonenumberExample as AppComponent };