Skip to content

feat: add web support for TransformerTextInput#12

Merged
janicduplessis merged 5 commits into
mainfrom
@janic/web-support
Jun 19, 2026
Merged

feat: add web support for TransformerTextInput#12
janicduplessis merged 5 commits into
mainfrom
@janic/web-support

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a web implementation of TransformerTextInput so the component works under react-native-web (Expo web, etc.). The native component is a Fabric decorator view plus worklets that run the transformer on the UI thread — neither exists on web, so importing the package on web previously broke the bundle (codegenNativeComponent / TurboModule aren't available under react-native-web).

What's in it

  • TransformerTextInput.web.tsx — an uncontrolled TextInput that runs transformer.worklet(...) synchronously in JS on each change and writes the formatted value + caret straight to the DOM node (mirroring the native side's imperative update). Web is single-threaded, so value and selection land together — no React re-render, and no controlled-input round-trip where the caret briefly jumps to the end. Reuses the existing computeUncontrolledSelection / validateSelection logic and exposes the same instance API (getValue / update / clear). No native decorator, TurboModule, or codegenNativeComponent.
  • TransformerTextInput.types.ts — the shared TransformerTextInputInstance / TransformerTextInputProps types extracted out, so the .web file can import them without resolving to itself; the native file re-exports them so the public API is unchanged.
  • Example app on all platforms — the example is now a full Expo app (mirroring the react-native-ease example): web via yarn web / yarn build:web (expo-router + react-native-web), and native via expo run:ios / expo run:android with continuous native generation (ios/ and android/ are gitignored and regenerated by expo prebuild). The bare RN CLI entry (index.js) is dropped in favor of the Expo entry on all platforms.

Test plan

  • yarn lint:ts (tsc) clean; yarn test (jest) — 122/122 pass, including the native snapshot.
  • Webyarn build:web exports cleanly; loaded the export in Chrome and typed into the phone field: formats as you type (4155550142+1 (415) 555-0142), deletes reformat correctly, all demo formatters render.
  • Androidexpo run:android builds the full native stack (the library's C++/JSI, reanimated, worklets, react-native-screens) and the demo renders on an emulator.
  • iOS — verified in CI (the build-ios job, Xcode 26 — required by Expo SDK 55): the library + example native build compiles. I didn't get a clean local iOS build (CocoaPods / Clang-module env issues on my machine), so there's no local render — CI is the iOS check.
Web Android
web android

Notes

  • The example uses Expo continuous native generation (ios/ and android/ are gitignored, regenerated by expo prebuild), so CI builds it on both platforms. Expo SDK 55 requires Xcode 26, so the build-ios job was bumped from 16.4.

Add a web implementation (TransformerTextInput.web.tsx): a controlled
TextInput that runs the transformer synchronously in JS and restores the
caret after each format. Web is single-threaded, so value and selection
update in one commit (no flicker) without the native decorator, TurboModule,
or codegenNativeComponent — none of which exist under react-native-web.
Shared types move to TransformerTextInput.types.ts so the .web file can
import them without resolving to itself.

Run the example on web (yarn web / yarn build:web) via expo-router +
react-native-web, mirroring the react-native-ease example.
Make the example a full Expo app so native and web build from one setup
(mirroring react-native-ease): native runs via `expo run:ios` / `expo
run:android` with continuous native generation — `ios/` and `android/` are
gitignored and regenerated by `expo prebuild` from app.json. Drops the bare
react-native CLI entry (index.js); the Expo entry is used on all platforms.

Verified: `expo run:android` builds (the library's native code, reanimated,
worklets, screens) and the demo renders on an emulator.
Write the formatted value and caret straight to the DOM node in the change
handler instead of going through React state. Web is single-threaded, so value
and selection land together synchronously — this drops the per-keystroke
re-render and removes the controlled-input round-trip (set value -> caret
collapses to end -> layout effect restores it), which had a one-frame window
where the caret could jump. Mirrors the native side's imperative update.
With the example using Expo continuous native generation, ios/ and android/
aren't checked in, which broke two CI jobs:

- lint ran `lint:android` (`cd example/android`), which no longer exists in a
  clean checkout. Drop it from the `lint` chain; `lint:ts` already typechecks
  the example (the root tsconfig has no `include`).
- build-ios had a separate "Install cocoapods" step doing `pod install` in
  example/ios, which isn't there until prebuild. Remove it — `build:ios`
  (`expo prebuild --clean`) generates the project and installs pods, matching
  react-native-ease. Target the x86_64 simulator slice on the macOS runner.
The CNG example pulls expo-modules-core from Expo SDK 55, whose Swift uses
strict-concurrency / MainActor features that Xcode 16.4 can't compile. Match
react-native-ease and build with Xcode 26.
@janicduplessis janicduplessis marked this pull request as ready for review June 19, 2026 03:28
@janicduplessis janicduplessis merged commit 3697760 into main Jun 19, 2026
5 checks passed
@janicduplessis janicduplessis deleted the @janic/web-support branch June 19, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant