Skip to content

Consider collapsing outlined/filled text inputs into one text-input element with a variant prop #26

Description

@shanerbaner82

Summary

We currently ship three separate text input elements — outlined-text-input, filled-text-input, bare-text-input — each with its own element class, Blade component, manifest entry, iOS renderer and Android renderer. outlined and filled are pure cosmetic variants of an identical contract and could collapse into a single <native:text-input variant="outlined|filled">, matching the pattern button already uses.

Filing this for awareness rather than as an urgent fix. Nothing is broken — this is API surface + maintenance cost.

Current state

Tag Chrome Honors element styles Per-instance color
outlined-text-input border, floating label, supporting text, icons
filled-text-input surface fill + bottom indicator, same decorations
bare-text-input none
text-input (core) n/a — see below n/a n/a

All three inherit behaviour (value sync, echo prevention, sync modes, secure/multiline, keyboard type, submit, disabled/readOnly) from BaseTextInput. OutlinedTextInput and FilledTextInput are 15-line subclasses that only override $type.

Why this looks cheap

1. button is the precedent. One button type, one renderer, a semantic variant prop, a switch variant inside NativeUIButtonRenderer. Same shape applies here.

2. The text_input type is unclaimed. Core explicitly reserves it for us:

// mobile-air src/NativeServiceProvider.php:682
// `button`, `text_input`, `toggle`, `activity_indicator`, `bottom_sheet`
// are registered by UI plugins (see nativephp/native-ui). Plugin
// discovery can't override an existing registration, so these must NOT be registered here.

Confirmed: nothing registers text_input in ElementRegistry, and no "text_input" renderer exists in a generated app on either platform. Edge/Elements/TextInput.php in core is benchmark-only. The name is free.

3. The native code is already mostly shared. NativeUIOutlinedTextInputRenderer.swift (179 loc) vs NativeUIFilledTextInputRenderer.swift (182 loc) differ by roughly 30 lines — border stroke vs surfaceVariant fill + indicator rectangle, plus slightly different SizeMetrics padding. Both already wrap NativeUITextInputCore.swift. On Android, TextInputShared.kt (249 loc) already holds parseTextInputProps, the echo-prevention sync and the sync-mode dispatcher for all three.

4. Dispatch is a generated string map. nativephp.jsonPluginRendererRegistration.{swift,kt}. Collapsing types is a manifest edit, not build-system surgery.

5. Back-compat is close to free. The manifest can keep outlined_text_input / filled_text_input registered against the same renderer, with the existing 15-line subclasses changing from "set $type" to "set default variant". Old tags keep working.

The caveat: bare should probably stay separate

bare isn't a visual variant — it has a different contract. BaseTextInput enforces a Model 3 style lockout:

// src/Elements/BaseTextInput.php:393
public function getStyle(): array { return []; }
public function getLayout(): array { $layout = parent::getLayout(); unset($layout['padding']); return $layout; }

BareTextInput (src/Elements/BareTextInput.php:79-87) overrides both to let element styles and padding through, and additionally accepts a per-instance color. Meanwhile it drops label, supporting, error, icons and prefix/suffix entirely — the iOS renderer is 88 loc precisely because it renders none of them.

Folding that into a variant means <text-input variant="bare" label="Email"> silently discards the label, and <text-input variant="outlined" class="rounded-full"> silently discards the radius. Which half of your attributes are live would depend on one other attribute, with no error. Three tags with honest contracts arguably document themselves better than one tag with a hidden mode switch.

Proposed shape

Unify outlined + filled, leave bare alone:

<native:text-input variant="outlined" label="Email" native:model.blur="email" />
<native:text-input variant="filled" label="Subject" native:model="subject" />

<native:bare-text-input class="flex-1 glass rounded-full px-4 py-2" placeholder="Message" native:model="draft" />

variant defaults to outlined. <outlined-text-input> / <filled-text-input> stay as deprecated aliases.

If we'd rather collapse all three anyway, the mitigation is to make the ignored props loud — throw or dev-warn when variant="bare" is combined with label / supporting / icons, turning the footgun into a build-time error.

Work sketch

  • src/Elements/TextInput.php — merge Outlined/Filled into one element with a variant prop; reduce the two existing classes to alias subclasses setting a default variant
  • src/Components/TextInput.php — Blade component for the new text_input type
  • nativephp.json — add text_input; repoint outlined_text_input / filled_text_input at the same renderers
  • iOS — merge the two renderers into NativeUITextInputRenderer with a switch variant around the chrome layer, sharing NativeUITextInputCore
  • Android — one renderer with when (variant) over the two composable call sites. Note OutlinedTextField and TextField are distinct composables with distinct Defaults.colors() types, so this branch can't be parameterized away; the real dedup already lives in TextInputShared.kt, so Android gains least here
  • mobile-air: src/Testing/TestableComponent.php:1169-1172 (type switch) and src/Validation/BladeTemplateAnalyzer.php:12,40,45 (element / SUPPORTS_CHANGE / SUPPORTS_SUBMIT lists) need the new type
  • Docs, boost guidelines, nativephp-mobile skill

Open questions

  • Do we take the text_input name, or keep the unified element under a different tag to avoid any confusion with core's benchmark-only Edge\Elements\TextInput?
  • Deprecate the old tags with a warning, or leave the aliases indefinitely?
  • Same question applies to other variant-ish element families — worth a quick audit for consistency with button.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions