<turbo-frame id="phone_number_default"> <form action="#" accept-charset="UTF-8" method="get"> <input type="hidden" name="name" id="name" value="phone" /><input type="hidden" name="value" id="value" /><input type="hidden" name="placeholder" id="placeholder" /><input type="hidden" name="readonly" id="readonly" value="false" /><input type="hidden" name="help" id="help" /><input type="hidden" name="error" id="error" /><input type="hidden" name="autocomplete_type" id="autocomplete_type" value="off" /><input type="hidden" name="mode" id="mode" value="inline" /><input type="hidden" name="initial_country" id="initial_country" value="pt" /><input type="hidden" name="separate_dial_code" id="separate_dial_code" value="true" /><input type="hidden" name="country_search" id="country_search" value="true" /><input type="hidden" name="show_flags" id="show_flags" value="true" /><input type="hidden" name="format_as_you_type" id="format_as_you_type" value="true" /><input type="hidden" name="strict_mode" id="strict_mode" value="true" /><input type="hidden" name="placeholder_number_type" id="placeholder_number_type" value="MOBILE" /><input type="hidden" name="submit_country_name" id="submit_country_name" /> <div class="w-80"> <div class="lui-phone-number lui-phone-number--inline relative" data-controller="phone-number" data-phone-number-initial-country-value="pt" data-phone-number-separate-dial-code-value="true" data-phone-number-country-search-value="true" data-phone-number-show-flags-value="true" data-phone-number-format-as-you-type-value="true" data-phone-number-strict-mode-value="true" data-phone-number-allowed-number-types-value="["MOBILE","FIXED_LINE"]" data-phone-number-placeholder-number-type-value="MOBILE" data-phone-number-country-name-locale-value="en" data-phone-number-has-country-input-value="false"> <div data-controller="input" data-input-open-actions-value="false" class="lui-inner-input relative flex gap-2" data-input-original-input-value="" data-input-mode-value="inline" data-input-form-value=""> <div class="w-full flex flex-col"> <div class="lui-input-chrome relative w-full"> <span class="lui-input "> <input name="_looposui-inputs-phonenumber_phone_5793709857" type="tel" class="lui-input__input" mode="inline" contentEditable="true" data-input-target="input" data-action="input->input#onChange change->input#onChange" data-phone-number-target="visibleInput" autocomplete="off"> <span class="lui-input__spinner"> <i class="fa-regular fa-spinner"></i> </span> </span> </div> </div> <span class="lui-inner-input__actions opacity-0 flex items-center gap-1 h-fit" data-input-target="actions"> <span class="lui-button__tooltip-wrapper inline-flex w-fit"> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="cancel" data-action="click->input#handleClose" type="button" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-xmark" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-rounded" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button></span> <span class="lui-button__tooltip-wrapper inline-flex w-fit"> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="submit" data-action="click->input#setLoading lui--button#startLoading" type="submit" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-check" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-rounded" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button></span> </span> </div> <input type="hidden" name="phone" id="phone" data-phone-number-target="hiddenInput" /> </div> </div> </form></turbo-frame>No Usage documentation to display.
<% sleep_enabled = preview_params.delete(:sleep) if sleep_enabled && params[preview_params[:name]].present? sleep(2) end%><%= turbo_frame_tag "phone_number_default" do %> <%= form_with url: "#", method: :get do %> <%= lookbook_fields(preview_params) %> <div class="w-80"> <%= render LooposUi::Inputs::PhoneNumber.new( **preview_params, value: params[preview_params[:name]].presence || preview_params[:value] ) %> </div> <% if params[preview_params[:name]].present? %> <p class="mt-2 text-sm text-gray-500"> Submitted (E.164): <strong><%= params[preview_params[:name]] %></strong> </p> <% end %> <% end %><% end %>No notes provided.
| Param | Description | Input |
|---|---|---|
|
Input name |
|
|
|
Initial phone value (E.164 recommended, e.g. +351912345678) |
|
|
|
Input placeholder |
|
|
|
Render as read-only |
|
|
|
Help text shown below the input |
|
|
|
Error message shown below the input |
|
|
|
HTML autocomplete token (e.g. tel, off) |
|
|
|
— |
|
|
|
ISO2 country code preselected (e.g. pt, gb, us) |
|
|
|
Show dial code beside the flag |
|
|
|
Enable searchable country selector |
|
|
|
Show country flags |
|
|
|
Format number as user types |
|
|
|
Restrict input to valid phone number characters |
|
|
|
— |
|
|
|
Hidden field name to store selected ISO2 country (optional) |
|
|
|
Sleep for 2 seconds, for testing purposes |
|
Description
LooposUi::Inputs::PhoneNumber is an international telephone number input component powered by intl-tel-input. It provides a country-flag selector, dial code display, real-time formatting, and client-side validation. The component submits phone numbers in E.164 format via a hidden field, keeping the visible input human-readable.
Dependencies
- JavaScript:
intl-tel-input(v29+), loaded lazily via dynamicimport("intl-tel-input/utils") - Stimulus controller:
phone-number(app/javascript/controllers/phone_number_controller.js) - CSS:
intl-tel-input/dist/css/intlTelInput.css(imported inapplication.postcss.css)
Arguments
Base Arguments
| Property | Default | Description |
|---|---|---|
model |
nil |
ActiveRecord instance. |
attribute |
nil |
Attribute name of the model. |
name |
nil |
Name of the hidden (submitted) field. Required when model is not provided. |
value |
nil |
Initial phone value. Prefer E.164 format (e.g. +351912345678) so the controller can restore the correct country. |
placeholder |
nil |
Placeholder shown in the visible input. |
error |
nil |
Error message. Automatically extracted from model errors when model is present. |
help |
nil |
Help text shown below the input. |
mode |
:inline |
:inline, :form, or :autosubmit. |
appearance |
nil |
Visual modifier. Defaults to mode when omitted. |
form |
nil |
HTML form attribute to associate the input with a specific form element. |
readonly |
false |
When true, renders the value as plain text without an editable input. |
custom_readonly |
false |
In readonly mode, renders provided content instead of the plain value. |
open_actions |
false |
Open field in edit mode by default (inline mode). |
extra_input_attributes |
{} |
Extra HTML attributes merged onto the visible <input type="tel"> element. |
Phone-specific Arguments
| Property | Default | Description |
|---|---|---|
autocomplete_type |
"off" |
HTML autocomplete token for the visible input (e.g. "tel", "off"). Disabled by default. |
initial_country |
"pt" |
ISO2 country code preselected on load (e.g. "pt", "gb", "us"). |
separate_dial_code |
true |
Show the dial code next to the flag in a separate box. |
country_search |
true |
Enable a search field in the country dropdown. |
show_flags |
true |
Show country flag icons. |
format_as_you_type |
true |
Format the phone number as the user types. |
strict_mode |
true |
Restrict input to valid phone number characters only. |
allowed_number_types |
["MOBILE", "FIXED_LINE"] |
Restrict validation to these number types. |
placeholder_number_type |
"MOBILE" |
Number type used to generate the placeholder example. Accepted: "MOBILE", "FIXED_LINE", "FIXED_LINE_OR_MOBILE". |
locale |
I18n.locale.to_s |
Locale for country name display (countryNameLocale). |
submit_country_name |
nil |
When provided, a second hidden input is rendered with this name to store the selected ISO2 country. |
Submit Value Strategy
The component renders two inputs:
- Visible
<input type="tel">— enhanced byintl-tel-inputfor formatting and country selection. Uses a safe internal name (never submitted directly). - Hidden
<input type="hidden">— carries the real field name and the E.164 formatted value. Updated by the Stimulus controller oninput,change,blur,countrychange, and formsubmitevents.
When intl-tel-input considers the number valid (iti.isValidNumber()), the hidden field receives the E.164 output of iti.getNumber() (e.g. +351912345678). When the number is invalid or the utils are not yet ready, the raw visible value is stored so the consuming application can validate and report errors.
If submit_country_name is set, a third hidden field is updated with the selected country ISO2 code (e.g. "pt").
Validation Note
- Client side:
intl-tel-inputformats and syncs the E.164 value in real time. Invalid numbers fall back to the raw visible value in the hidden field. - Server side: Final validation is the responsibility of the consuming application. Treat client-side formatting as a convenience, not as the source of truth.
Usage
Example using name + value
<%= render LooposUi::Inputs::PhoneNumber.new( name: "contact_phone", value: "+351912345678", initial_country: "pt", placeholder: "Enter phone number", help: "Include country code") %>Example using model + attribute
<%= render LooposUi::Inputs::PhoneNumber.new( model: @contact, attribute: :phone_number, initial_country: "gb") %>The hidden input will use the Rails field name contact[phone_number] and pick up the model's current value and errors automatically.
Example with submit_country_name
<%= render LooposUi::Inputs::PhoneNumber.new( name: "phone", value: nil, initial_country: "us", submit_country_name: "phone_country_code") %>This renders an extra <input type="hidden" name="phone_country_code"> that is updated with the selected ISO2 country code (e.g. "us") on every change.
⚠️ Important
- Pass existing phone numbers as E.164 strings (e.g.
"+351912345678") so the Stimulus controller can restore the correct country flag and national format on page load. initial_countryaccepts lowercase ISO2 codes only (e.g."pt","gb","us").- JS behavior (country picker, real-time sync) requires the
intl-tel-inputpackage and thephone-numberStimulus controller to be loaded. Verify withyarn build/yarn build:css. - Readonly mode renders the raw value as text; no JS enhancement is applied.