Phone input

bs-phone-input is a phone-number field: a country picker with flags, a dial code that cannot be edited away, and as-you-type formatting. The bound value is E.164 — +32470123456 — or null while empty, so a form posts one canonical string.

Validation rules are loaded per calling code on first interaction, so a field costs about 20 kB gzip when the user reaches it and nothing before that. (phonechange) reports valid as undefined until those rules arrive — deliberately, rather than claiming a number is valid before anything has checked it.

Basic usage

+32

Bound value (E.164):

null

Last (phonechange) detail:

null
  
    
      
        <bs-form>
        
    
      
          <bs-phone-input [(ngModel)]="phone" name="phone"
        
    
      
                          defaultCountry="be"
        
    
      
                          (phonechange)="onPhoneChange($event)" />
        
    
      
        </bs-form>
        
    
  
    
      
        import { Component, model } from '@angular/core';
        
    
      
        import { FormsModule } from '@angular/forms';
        
    
      
        import { BsFormComponent } from '@mintplayer/ng-bootstrap/form';
        
    
      
        import { BsPhoneInputComponent } from '@mintplayer/ng-bootstrap/phone-input';
        
    
      
        import type { PhoneChangeEventDetail } from '@mintplayer/ng-bootstrap/phone-input';
        
    
      
        
        
    
      
        @Component({
        
    
      
          selector: 'my-phone-demo',
        
    
      
          templateUrl: './my-phone-demo.component.html',
        
    
      
          imports: [FormsModule, BsFormComponent, BsPhoneInputComponent],
        
    
      
        })
        
    
      
        export class MyPhoneDemoComponent {
        
    
      
          // The bound value is E.164 — '+32470123456' — or null while empty.
        
    
      
          readonly phone = model<string | null>(null);
        
    
      
        
        
    
      
          onPhoneChange(detail: PhoneChangeEventDetail) {
        
    
      
            // detail.valid is undefined until the country's rules have loaded.
        
    
      
            console.log(detail.country, detail.nationalNumber, detail.valid);
        
    
      
          }
        
    
      
        }
        
    

Localized country names

Country names come from Intl.DisplayNames, so they are localized and collated in the viewer's language at no download cost. Leave locale unset to follow the browser — the default, and what most apps should ship.

Open the picker above after switching: the list re-sorts, because an alphabetical list of English names is not alphabetical to a Dutch reader.

Restricting and pinning countries

+31
null
  
    
      
        <!-- allowed-countries, NOT only-countries: Angular refuses to bind any
        
    
      
             attribute whose name starts with 'on'. -->
        
    
      
        <bs-phone-input [(ngModel)]="phone"
        
    
      
                        [allowedCountries]="['be', 'nl', 'lu']"
        
    
      
                        [preferredCountries]="['nl']"
        
    
      
                        defaultCountry="nl" />
        
    

Inside an input group

bs-input-group joins any mix of controls into one continuous field, including a whole phone input: its outer corners pair with the neighbouring addon and the borders collapse.

Tel
+32
  
    
      
        <!-- bs-input-group joins any mix of controls, including a whole phone
        
    
      
             input: the corners pair and the borders collapse.
        
    
      
             Below ~6rem per control the row wraps instead of crushing them;
        
    
      
             override --mp-group-min-item-width for a denser toolbar. -->
        
    
      
        <bs-input-group>
        
    
      
          <span class="addon">Tel</span>
        
    
      
          <bs-phone-input [(ngModel)]="phone" defaultCountry="be" />
        
    
      
        </bs-input-group>
        
    

Validation

Tick Required above and blur the empty field. The message from errorMessages is rendered under the field and referenced by aria-errormessage and aria-describedby from the number input, so a screen reader reads it with the field.

  
    
      
        // The control's errors reach the field through BsControlValidityDirective,
        
    
      
        // which the component applies itself: the message renders under the field,
        
    
      
        // referenced by aria-errormessage from the number input.
        
    
      
        <bs-phone-input [(ngModel)]="phone" name="phone" required
        
    
      
                        [errorMessages]="{ required: 'A phone number is required.' }" />
        
    

Keyboard

  • Tab — two stops: the country picker, then the number field.
  • In the picker: ↑/↓ to move, type a country name to jump to it, Enter to choose, Esc to close. Choosing a country moves focus to the number field.
  • In the number field: Backspace/Delete always remove a digit, never a space or bracket, so one keypress is never swallowed by the formatter.
  • Pasting or typing +44 … switches the country and strips the dial code.

Angular forms

Template-driven [(ngModel)] and reactive [formControl] both bind through the component's ControlValueAccessor: the value, the disabled state, and touched when focus leaves.

Template-driven

+32
Value: null
dirty: false · touched: false · disabled: false

Reactive

+32
Value: null
dirty: false · touched: false · disabled: false
  
    
      
        <!-- template-driven (FormsModule) -->
        
    
      
        <bs-phone-input [(ngModel)]="value" defaultCountry="be"></bs-phone-input>
        
    
      
        
        
    
      
        <!-- reactive (ReactiveFormsModule): control = new FormControl(...) -->
        
    
      
        <bs-phone-input [formControl]="control" defaultCountry="be"></bs-phone-input>