# TextInput example

> Controlled text input in React, Vue, Angular, Svelte, and Solid.

`text-input` is controlled through a `value` prop and a text-change event. The
controlled write handshake is shared; the public event name is framework-shaped.

<Tabs syncKey="framework">
  <TabItem label="React">
    ```tsx
    import './NameInput.css';
    import { useState } from 'react';

    export function NameInput() {
      const [name, setName] = useState('Symbiote');

      return (
        <view className="root">
          <text-input value={name} onValueChange={event => setName(event.text)} className="input" />
          <text>Hello, {name}</text>
        </view>
      );
    }
    ```

    ```css
    /* NameInput.css */
    .root { gap: 12px; }
    .input { min-width: 220px; padding: 12px; border-width: 1px; }
    ```

  </TabItem>
  <TabItem label="Vue">
    ```vue
    <script setup lang="ts">
    import { ref } from 'vue';

    const name = ref('Symbiote');
    </script>

    <template>
      <view class="root">
        <text-input v-model="name" class="input" />
        <text>Hello, {{ name }}</text>
      </view>
    </template>

    <style scoped>
    .root { gap: 12px; }
    .input { min-width: 220px; padding: 12px; border-width: 1px; }
    </style>
    ```

    `v-model="name"` is sugar over the same `:value`/`@value-change` pair — write
    that explicit form instead if you also need the raw native event:
    `@value-change="event => setName(event.text)"`. The event handed to
    `@value-change` IS the native change event, with `text` merged onto it —
    there is no separate second argument.

  </TabItem>
  <TabItem label="Angular">
    ```ts
    import { Component } from '@angular/core';
    import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
    import './name-input.css';

    @Component({
      standalone: true,
      imports: [SYMBIOTE_ELEMENTS],
      template: `
        <view class="root">
          <text-input [(value)]="name" class="input" />
          <text>Hello, {{ name }}</text>
        </view>
      `,
    })
    export class NameInput {
      name = 'Symbiote';
    }
    ```

    ```css
    /* name-input.css */
    .root { gap: 12px; }
    .input { min-width: 220px; padding: 12px; border-width: 1px; }
    ```

    `[(value)]="name"` is Angular's banana-in-a-box two-way binding, sugar over
    the same `[value]`/`(valueChange)` pair — write that explicit form instead
    (`[value]="name" (valueChange)="setName($event)"`) if you also need the raw
    native event, which arrives on the separate `(change)` output: an
    `EventEmitter` only carries one value, so `(valueChange)` stays text-only to
    keep `[(value)]` working, and `(change)` exists purely for the raw event.

  </TabItem>
  <TabItem label="Svelte">
    ```svelte
    <script lang="ts">
      let name = $state('Symbiote');
    </script>

    <view class="root">
      <text-input
        value={name}
        onValueChange={event => (name = event.text)}
        class="input"
      ></text-input>
      <text>Hello, {name}</text>
    </view>

    <style>
      .root { gap: 12px; }
      .input { min-width: 220px; padding: 12px; border-width: 1px; }
    </style>
    ```

    Svelte's own element `bind:value` is explicitly unsupported by this
    adapter's DOM shim — there is no sugar over `value`/`onValueChange` here,
    same as React. Unlike a plain `onPress`, `onValueChange` is safe to write
    as an individual attribute directly: it is always called with a single
    change-event object, so it never trips the crash an individual callback
    prop can otherwise hit on a bare host tag.

  </TabItem>
  <TabItem label="Solid">
    ```tsx
    import './NameInput.css';
    import { createSignal } from 'solid-js';

    export function NameInput() {
      const [name, setName] = createSignal('Symbiote');

      return (
        <view class="root">
          <text-input value={name()} onValueChange={event => setName(event.text)} class="input" />
          <text>Hello, {name()}</text>
        </view>
      );
    }
    ```

    ```css
    /* NameInput.css */
    .root { gap: 12px; }
    .input { min-width: 220px; padding: 12px; border-width: 1px; }
    ```

    No `v-model`-style sugar here either: `value`/`onValueChange` is the
    whole contract, same as React and Svelte.

  </TabItem>
</Tabs>

## API takeaway

| Concern           | React                          | Vue                               | Angular                                               | Svelte                                | Solid                          |
| ------------------ | ------------------------------- | ---------------------------------- | ------------------------------------------------------ | --------------------------------------- | -------------------------------- |
| Text prop         | `value`                        | `:value`<br />or `v-model`         | `[value]`<br />or `[(value)]`                          | `value`                                 | `value`                          |
| Text change       | `onValueChange={event => …}`   | `@value-change`<br />or `v-model`  | `(valueChange)="handler($event)"` (text only)          | `onValueChange={event => …}`            | `onValueChange={event => …}`     |
| Native event      | _(same event object)_          | _(same emit, same event object)_   | `(change)="handler($event)"` (separate output)         | _(same event object)_                   | _(same event object)_            |

All five now share one contract: `onValueChange` (or its per-adapter emit/output
spelling) is called with a **single** change-event object, `event.text` carries
the current text, and `event` itself is the native change event — there is no
separate second argument anywhere. Angular is the one exception, since its
`(valueChange)` output is deliberately text-only to keep `[(value)]` working,
with `(change)` carrying the full event instead.

`StyleSheet.create` works identically to a CSS class if you'd rather keep
style objects inline — see the [Styling guide](/docs/learn/styling/).
