Skip to content

DOM adapter

@telixon/web-sdk is the layer between a browser <input> and a headless input controller. It listens for the native editing events the browser fires, translates each into an editing intent the controller understands, and writes the controller’s next state back to the element. The controller holds all phone-number logic; the adapter holds only the DOM wiring.

The adapter attaches a beforeinput listener and calls event.preventDefault() on every editing event it handles. The browser’s own edit never runs; the adapter reads the current value, selectionStart, and selectionEnd, dispatches the matching controller intent, and applies the result. It classifies the event by its inputType:

inputType Controller intent
insertText, insertReplacementText, insertFromPaste, insertFromDrop, insertFromYank insert with the event text over the selection
deleteContentBackward, deleteByCut, deleteContent, deleteByDrag deleteBackward
deleteContentForward deleteForward
deleteWordBackward deleteBackward over the previous word boundary
deleteWordForward deleteForward over the next word boundary
deleteSoftLineBackward, deleteHardLineBackward deleteBackward from the line start to the caret
deleteSoftLineForward, deleteHardLineForward deleteForward from the caret to the line end
deleteEntireSoftLine, deleteEntireHardLine deleteBackward over the whole value
historyUndo undo, when an undo step is available
historyRedo redo, when a redo step is available

Insert events cover typing, paste, drop, and dictation replacement. When event.data is absent, as with a paste or drop, the text is read from event.dataTransfer instead; an empty result inserts nothing. Word deletes resolve their boundary from the current value: with a collapsed caret the adapter walks past adjacent separators and then over the digit group, so a word delete always consumes a run of digits rather than a stray separator.

Any inputType not in the table falls through untouched. Because the event was prevented, an unhandled edit is a no-op rather than a raw character reaching the field.

Undo and redo reach the controller two ways. Browsers that emit historyUndo and historyRedo through beforeinput route through the table above. As a fallback, the adapter also listens for the Ctrl/Cmd keyboard shortcuts (Z, Shift+Z, Y) and maps them to the controller’s undo and redo. Either path replays the controller’s own history, so the platform gesture and the programmatic undo()/redo() methods restore the same states, caret included.

While an input method editor is composing, the adapter stays out of the way. beforeinput events carry isComposing: true during composition, and the adapter ignores them: the intermediate composition string is not phone input and must not be reformatted mid-stroke. When composition finishes, the compositionend event delivers the committed text, and the adapter inserts that text as a single insert intent over the selection recorded on the element. Composition that commits no text produces no edit.

The adapter never lets the browser mutate input.value and never diffs an old string against a new one. It reports the operation the user performed, the current value, the inserted or deleted text, and the selection, and the controller returns the next state. The adapter’s only write-back is to set input.value and the caret range from that state.

Routing intents rather than reformatting a settled string is what keeps the caret exact. The controller tracks the caret in digit space and maps it back through the format mask, so it knows where the caret belongs after a reflow. A diff of the rendered string could not recover that, since reformatting moves separators the user never touched. This is why paste and drop go through insert with the transferred text rather than being written to the field directly.

One phone input attaches per element. The adapter records each attached element in a WeakSet, and a second createPhoneInput on the same element throws. destroy() removes the element from the set and detaches every listener, so the same element can be attached again afterward. destroy() is idempotent.

The element must be an <input type="text"> or <input type="tel">; any other type throws. Construction also requires a ready engine, so call ensureEngineReady() before createPhoneInput.

The adapter writes exactly two things to the element: value and the selection range. Everything else is data on the emitted PhoneInputState for the host to render.

  • The placeholder is exposed as state.placeholder and never written to the element’s placeholder attribute. See Placeholders.
  • Styling, class names, and the field’s visual state are yours. The package ships no CSS.
  • Labels, the region picker, and any surrounding markup are yours. state.region, state.validationError, and the filter values are the inputs to that rendering.