이 페이지는 아직 번역되지 않았습니다. 영어 원문 보기

요소

Input

Input is a native text field: a UITextField (single line) or UITextView (multiline) on iOS and Mac Catalyst, an EditText on Android. Typing, the caret, selection, autocorrection and autofill are the platform’s own. Rust makes one with nsh_base::input(), TSX with <Input> from nsh/base.

el-input.tsx
el-input.tsxLive
import { type InputRef, type JSX, mount, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { button, buttonText, field, Inspector, Panel, Stage, ui, wide } from "./palette";

function App() {
  const name = signal("");
  const password = signal("");
  const notes = signal("");
  const submitted = signal("");
  let passwordInput!: InputRef;
  const signIn = () => submitted.set(`Signed in as ${name() || "nobody"}`);
  const nameField = () => (
    <Input
      {...field()}
      value={name()}
      onChange={({ value }) => name.set(value)}
      placeholder="Ada Lovelace"
      autoCapitalize="words"
      maxLength={20}
      returnKeyType="next"
      onSubmit={() => passwordInput.focus()}
    />
  );
  const count = () => <Text style={{ fontSize: 12, color: ui().muted }}>{name().length} / 20</Text>;
  const passwordField = () => (
    <Input
      ref={passwordInput}
      {...field()}
      value={password()}
      onChange={({ value }) => password.set(value)}
      masked
      autoComplete="current-password"
      returnKeyType="done"
      onSubmit={signIn}
    />
  );
  const notesField = () => (
    <Input
      {...field({ style: { height: wide() ? 80 : 100 } })}
      multiline
      value={notes()}
      onChange={({ value }) => notes.set(value)}
      placeholder="Anything else?"
    />
  );
  const signInButton = () => (
    <View onPress={signIn} disabled={password().length === 0} {...button("primary", { style: { marginTop: 4 } })}>
      <Text style={buttonText("primary")}>Sign in</Text>
    </View>
  );
  /** A label above its field, and what describes it below. */
  const group = (label: string, input: JSX.Element, after?: JSX.Element) => (
    <View style={{ gap: 6 }}>
      <Text style={{ fontSize: 13, color: ui().muted }}>{label}</Text>
      {input}
      {after}
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, padding: 20, paddingTop: 24, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>
            <Text style={{ color: ui().muted }}>Tab moves between the fields; Return in the name goes to the password</Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <Stage width={340} gap={16}>
                {group("Name (controlled, maxLength 20)", nameField(), count())}
                {group("Password", passwordField())}
                {group("Notes (multiline)", notesField())}
                {signInButton()}
              </Stage>
              <Inspector>
                <Panel title="value">
                  <Text style={{ fontSize: 13, color: ui().text }}>name {name() ? `“${name()}”` : "—"}</Text>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>password {password().length} characters</Text>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>notes {notes().length} characters</Text>
                </Panel>
                <Panel title="onSubmit" grow>
                  <Text style={{ fontSize: 13, color: ui().up }}>{submitted()}</Text>
                </Panel>
              </Inspector>
            </View>
          </View>
        ) : (
          <View style={{ flex: 1, width: "100%", maxWidth: 480, alignSelf: "center", padding: 20, paddingTop: 24, gap: 10 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>

            <Text style={{ color: ui().muted }}>Name (controlled, maxLength 20)</Text>
            {nameField()}
            {count()}

            <Text style={{ color: ui().muted }}>Password</Text>
            {passwordField()}

            <Text style={{ color: ui().muted }}>Notes (multiline)</Text>
            {notesField()}

            {signInButton()}
            <Text style={{ color: ui().up }}>{submitted()}</Text>
          </View>
        )
      }
    </View>
  );
}

mount(App);
use nsh_base::input;
use nsh_core::prelude::*;

fn name_field() -> impl IntoElement {
    let name = signal(String::new());
    input()
        .value(name)
        .on_change(move |text: &str| name.set(text.to_string()))
        .placeholder("Your name")
        .auto_capitalize(AutoCapitalize::Words)
        .return_key_type(ReturnKeyType::Done)
        .text_base()
        .p_3()
        .border_1()
        .border_color(rgb(0xd0d7de))
        .rounded(px(8.0))
}

#Controlled and uncontrolled

  • Controlled: pass value and update it in onChange. The app can rewrite what the user typed (trim it, format it): the text shows the edit at once and is corrected when the new value arrives. Each native edit carries a count, and a value from the app is applied when it is based on the latest edit, so newer typing always wins over a slow update.
  • Uncontrolled: pass defaultValue (.default_value(..), or nothing) and read the text from onChange or onSubmit.

Rules that must hold while the user types are props, answered natively: maxLength limits the length and submitBehavior decides what return does. See forms and the keyboard.

#Props

Besides the common props (with style typed as TextStyle: font, color, alignment and the view styles apply):

#Text

PropRustTypeDefaultDescription
value.value(..)stringThe text, controlled by the app.
defaultValue.default_value(..)stringThe initial text.
placeholder.placeholder(..)stringShown while the input is empty.
multiline.multiline(..)booleanfalseSeveral lines; return inserts a line break unless submitBehavior says otherwise.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueA multiline input’s scroll bar: the platform’s indicator, or the app’s built-in bar once it set one (Scrollbar); an object is a theme of its own, false none.
maxLength.max_length(..)numberThe most characters the user can enter.
readonly.readonly(..)booleanfalseThe user can select and copy the text but not change it.
masked.masked(..)booleanfalseThe text is hidden (passwords).
allowFontScalingbooleantrueFollow the system text size.
maxFontSizeMultipliernumber0The upper bound of the system scale (0 is none).

#Keyboard

PropRustTypeDefaultDescription
keyboardType.keyboard_type(KeyboardType::Email)"default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url""default"The keyboard layout (the values; numbers-and-punctuation has a minus sign on iOS).
returnKeyType.return_key_type(ReturnKeyType::Next)"default" | "done" | "go" | "next" | "search" | "send""default"The label of the return key.
autoCapitalize.auto_capitalize(AutoCapitalize::None)"none" | "sentences" | "words" | "characters""sentences"Automatic capitalization.
autoCorrect.auto_correct(..)booleantrueThe system’s corrections.
autoComplete.auto_fill(AutoFill::OneTimeCode)AutoComplete | TextContentTypeWhat the input holds, for autofill on iOS, Android and the web: "username", "current-password", "new-password", "one-time-code", "email", "tel", "postal-code", "cc-number", "off", …
pasteBehavior.paste_behavior(PasteBehavior::Report)"insert" | "report""insert"What a paste does: "insert" inserts the pasted text, "report" inserts nothing, for the app to handle what onPaste reports. Images and files are never inserted.
submitBehavior.submit_behavior(SubmitBehavior::Submit)"submit" | "blurAndSubmit" | "newline""blurAndSubmit" (single line), "newline" (multiline)What return does: "submit" reports onSubmit and keeps the focus, "blurAndSubmit" also unfocuses, "newline" inserts a line break (multiline; a single line submits). A multiline input that submits still takes line breaks from Shift+Return or Option / Alt+Return on hardware keyboards. "submit" keeps the keyboard up (e.g. to send several messages).
autoFocus.auto_focus(..)booleanfalseFocused when it appears.

#Caret and selection

PropRustTypeDescription
caret.caret(..)booleanThe caret is drawn (default true; false hides it).
selectedRange.selected_range((start, end)){ start: number; end?: number }The controlled selection, in UTF-16 units; end defaults to start (a caret).

The caret blinks while its window is shown and active. In GPUI windows on Linux it stays shown after ten seconds without typing or moving it, as in GTK apps, so a focused field left alone costs no frames.

#Colors

The colors of the placeholder, the caret and the selection highlight are style keys, with their Tailwind classes, and methods in Rust (UIKit tints the caret and the selection with one color, the caret’s when set):

Style keyRustClassDescription
placeholderColor.placeholder_color(..)placeholder:text-*The placeholder’s color.
caretColor.caret_color(..)caret-*The caret’s color.
selectionColor.selection_color(..)selection:text-*The selection highlight’s color.
input()
    .placeholder("Search")
    .text_color(rgb(0x18181b))
    .placeholder_color(rgb(0x9ca3af))
    .caret_color(rgb(0x2563eb))
    .selection_color(rgb(0x93c5fd))

#iOS

ios keyRustTypeDescription
textContentType.auto_fill(..)AutoComplete | TextContentTypeiOS’s names for autoComplete ("oneTimeCode", "newPassword", …).
inputAccessoryViewID.input_accessory(id)stringThe nativeID of an InputAccessoryView (InputAccessoryView) shown over the keyboard while the input has focus.
keyboardAppearance.keyboard_appearance(KeyboardAppearance::Dark)"default" | "light" | "dark"The keyboard’s look; "default" follows the system’s appearance. Android’s keyboards follow the system’s theme.
use nsh_core::element::AutoFill;

input().auto_fill(AutoFill::OneTimeCode).keyboard_type(KeyboardType::Number)

#Events

EventRustPayloadWhen
onChange.on_change(|text: &str| ..){ value }The user changed the text: value is the text now.
onSubmit.on_submit(|text: &str| ..){ text }The return key was pressed in an input that submits (single line, or submitBehavior "submit" / "blurAndSubmit").
onFocus / onBlur.on_focus / .on_blurnoneThe input gained / lost focus.
onSelectionChange.on_selection_change(|s: &Selection| ..){ start, end }The caret or selection moved (UTF-16 units).
onPaste.on_paste(|e: &PasteEvent| ..){ text, html, files, uris }The user pasted: the text, its formatting as HTML (Linux, web), images (a screenshot) as Files, local files’ file: URLs. Before the onChange of the text it inserts (forms and keyboard).
onCaretMove.on_caret_move(|e: &CaretEvent| ..){ bounds, pageBounds }The caret moved, with the selection or as the text scrolled or wrapped: bounds is where it is in the input, pageBounds in the window, an Overlay’s anchor (forms and keyboard).
onKeyPress.on_key_press(|key: &str| ..){ key }Before the text changes: key is the typed text, "Backspace" or "Enter".
onContentSizeChange.on_content_size_change(|size: &Size<Points>| ..){ size }The size of the text changed (at the input’s width when multiline), as it is typed; an empty input reports one empty line, whatever its placeholder.
onCompositionChange.on_composition_change(|e: &Composition| ..){ composing, text }An input method’s composition started, changed or ended: text is what is being composed (pinyin before a character is chosen, kana before conversion), "" once it ends. The committed text reaches onChange before the end.

A multiline input that grows with its text:

let height = signal(px(40.0));

input()
    .multiline(true)
    .on_content_size_change(move |size: &Size<Points>| {
        height.set(px((size.height.0 + 16.0).clamp(40.0, 120.0)));
    })
    .h(move || height.get())
    .px_3()
    .py_2()

#Ref methods

MethodRustDescription
focus()focus()Focuses the input and shows the keyboard.
blur()blur()Unfocuses it.
setSelection(start, end?)set_selection(start, end)Selects start..end (UTF-16 units); a caret when they are equal.

Moving focus to the next field on return:

use nsh_core::element::AutoFill;

let password = element_ref();

view()
    .child(input().return_key_type(ReturnKeyType::Next).on_submit(move |_: &str| password.focus()))
    .child(input().element_ref(&password).masked(true).auto_fill(AutoFill::Password))

Keyboard.dismiss() from nsh (dismiss_keyboard() in Rust) hides the keyboard whichever input has it.

#Platform notes

  • On macOS, single-line text, placeholders and carets share the native field’s font metrics and are centered together inside the content box. lineHeight participates in layout; multiline editors also use it for paragraph spacing.
  • Inputs inside a scroll view are kept clear of the keyboard natively. Elsewhere use a bottom padding or margin of env(keyboard-inset-height).
  • disabled disables the input (no focus) as well as applying the disabled styles; a readonly input keeps its focus and selection.
  • inputAccessoryViewID applies on iOS, where the keyboard has an accessory bar.