元素

Input

Input 是原生文本输入框:iOS 和 Mac Catalyst 上是 UITextField(单行)或 UITextView(多行),Android 上是 EditText。输入、光标、选区、自动更正和自动填充都由平台自己处理。Rust 用 nsh_base::input() 创建,TSX 用从 nsh/base 导入的 <Input>。

el-input.tsx
el-input.tsx运行中
import { type JSX, mount, signal, Text, View, type YNode } 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!: YNode;
  const signIn = () => submitted.set(`Signed in as ${name() || "nobody"}`);
  const nameField = () => (
    <Input
      {...field()}
      value={name()}
      onChange={(text) => name.set(text)}
      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={(text) => password.set(text)}
      masked
      autoComplete="current-password"
      returnKeyType="done"
      onSubmit={signIn}
    />
  );
  const notesField = () => (
    <Input
      {...field({ style: { height: wide() ? 80 : 100 } })}
      multiline
      value={notes()}
      onChange={(text) => notes.set(text)}
      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("你的名字")
        .auto_capitalize(AutoCapitalize::Words)
        .return_key_type(ReturnKeyType::Done)
        .text_base()
        .p_3()
        .border_1()
        .border_color(rgb(0xd0d7de))
        .rounded(px(8.0))
}

#受控与非受控

  • 受控:传入 value,并在 onChange 中更新它。应用可以改写用户的输入(去掉空白、格式化):输入框会立即显示用户的编辑,新的 value 到达后再被修正。每次原生编辑都带有一个计数,基于最新一次编辑的 value 才会被应用,因此用户更新的输入总是优先于较慢的更新。
  • 非受控:传入 defaultValue(.default_value(..),或不传),从 onChange 或 onSubmit 读取文本。

用户输入过程中必须遵守的规则以属性形式给出,由原生侧直接判断:maxLength 限制长度,submitBehavior 决定回车键的行为。参见表单与键盘。

#属性

除通用属性外(style 的类型为 TextStyle:字体、颜色、对齐方式和视图样式都适用):

#文本

属性Rust类型默认值说明
value.value(..)string由应用控制的文本。
defaultValue.default_value(..)string初始文本。
placeholder.placeholder(..)string输入框为空时显示的提示。
multiline.multiline(..)booleanfalse多行;回车插入换行(submitBehavior 另有设置时除外)。
maxLength.max_length(..)number用户最多能输入的字符数。
readonly.readonly(..)booleanfalse用户可以选择和复制文本,但不能修改。
masked.masked(..)booleanfalse隐藏文本(密码)。
allowFontScalingbooleantrue跟随系统文字大小。
maxFontSizeMultipliernumber0系统缩放的上限(0 表示不限)。

#键盘

属性Rust类型默认值说明
keyboardType.keyboard_type(KeyboardType::Email)"default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url""default"键盘布局(各取值;numbers-and-punctuation 在 iOS 上有负号)。
returnKeyType.return_key_type(ReturnKeyType::Next)"default" | "done" | "go" | "next" | "search" | "send""default"回车键的标签。
autoCapitalize.auto_capitalize(AutoCapitalize::None)"none" | "sentences" | "words" | "characters""sentences"自动大写。
autoCorrect.auto_correct(..)booleantrue系统自动更正。
autoComplete.auto_fill(AutoFill::OneTimeCode)AutoComplete | TextContentType输入框内容的类型,用于自动填充:"username"、"current-password"、"new-password"、"one-time-code"、"email"、"tel"、"postal-code"、"cc-number"、"off" 等。
pasteBehavior.paste_behavior(PasteBehavior::Report)"insert" | "report""insert"粘贴做什么:"insert" 插入粘贴的文本,"report" 不插入任何内容,由应用处理 onPaste 报告的内容。图片和文件从不插入。
submitBehavior.submit_behavior(SubmitBehavior::Submit)"submit" | "blurAndSubmit" | "newline""blurAndSubmit"(单行),"newline"(多行)回车键做什么:"submit" 触发 onSubmit 并保持焦点,"blurAndSubmit" 提交后再失去焦点,"newline" 插入换行(多行;单行总是提交)。会提交的多行输入框在硬件键盘上仍可用 Shift+Return 或 Option / Alt+Return 换行。"submit" 让键盘保持弹出(例如连续发送多条消息)。
autoFocus.auto_focus(..)booleanfalse出现时自动获得焦点。

#光标与选区

属性Rust类型说明
caret.caret(..)boolean绘制光标(默认 true;false 隐藏光标)。
selectedRange.selected_range((start, end)){ start: number; end?: number }受控选区,以 UTF-16 为单位;end 默认等于 start(即光标)。

#颜色

提示文字的颜色以及光标和选区的颜色是样式键,有对应的 Tailwind 变体,在 Rust 中是方法:

样式键Rust类说明
placeholderColor.placeholder_color(..)placeholder:text-*提示文字的颜色。
selectionColor.selection_color(..)selection:text-*光标和选区的颜色。
input()
    .placeholder("搜索")
    .text_color(rgb(0x18181b))
    .placeholder_color(rgb(0x9ca3af))
    .selection_color(rgb(0x2563eb))

#iOS

ios 键Rust类型说明
textContentType.auto_fill(..)AutoComplete | TextContentTypeautoComplete 在 iOS 上的名称("oneTimeCode"、"newPassword" 等)。
inputAccessoryViewID.input_accessory(id)string一个 InputAccessoryView(InputAccessoryView)的 nativeID,输入框获得焦点时显示在键盘上方。
keyboardAppearance.keyboard_appearance(KeyboardAppearance::Dark)"default" | "light" | "dark"键盘的外观;"default" 跟随系统外观。Android 的键盘跟随系统主题。
use nsh_core::element::AutoFill;

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

#事件

事件Rust载荷触发时机
onChange.on_change(|text: &str| ..)(text: string)用户修改了文本。
onSubmit.on_submit(|text: &str| ..){ text }在会提交的输入框中按下了回车键(单行,或 submitBehavior 为 "submit" / "blurAndSubmit")。
onFocus / onBlur.on_focus / .on_blur无输入框获得 / 失去焦点。
onSelectionChange.on_selection_change(|s: &Selection| ..){ start, end }光标或选区移动(UTF-16 单位)。
onPaste.on_paste(|e: &PasteEvent| ..){ text, files, uris }用户粘贴了内容:文本、作为 File 的图片(如截图)、本地文件的 file: URL。在它插入的文本的 onChange 之前(表单与键盘)。
onCaretMove.on_caret_move(|e: &CaretEvent| ..){ bounds, pageBounds }光标随选区移动,或因文本滚动、重新换行而移动:bounds 是它在输入框中的位置,pageBounds 是它在窗口中的位置,可作为 Overlay 的 anchor(表单与键盘)。
onKeyPress.on_key_press(|key: &str| ..){ key }文本变化之前:key 是输入的文本、"Backspace" 或 "Enter"。
onContentSizeChange.on_content_size_change(|size: &Size<Points>| ..){ width, height }文本尺寸变化(多行时按输入框宽度计算),随输入报告;输入框为空时报告一行空行的尺寸,与占位文字无关。
onCompositionChange.on_composition_change(|e: &Composition| ..){ composing, text }输入法的组字开始、变化或结束:text 是正在组的字(选字前的拼音、变换前的假名),结束时为 ""。上屏的文本先经 onChange 报告,然后才报告结束。

随文本增高的多行输入框:

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 方法

方法Rust说明
focus()focus()让输入框获得焦点并弹出键盘。
blur()blur()让它失去焦点。
setSelection(start, end?)set_selection(start, end)选中 start..end(UTF-16 单位);两者相等时为光标。

按回车把焦点移到下一个输入框:

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))

无论哪个输入框持有键盘,nsh 的 Keyboard.dismiss()(Rust 中为 dismiss_keyboard())都能把它收起。

#平台说明

  • 在 macOS 上,单行文字、提示文字和光标使用原生输入框一致的字体度量,在内容区域内一起垂直居中。 lineHeight 参与布局;多行编辑器同时用它控制段落行高。
  • 滚动视图中的输入框会在原生侧自动避开键盘。其他位置请使用 env(keyboard-inset-height) 作为底部内边距或外边距。
  • disabled 除了应用禁用样式,还会禁用输入框(不能获得焦点);readonly 的输入框仍可获得焦点和选择文本。
  • inputAccessoryViewID 在 iOS 上生效,那里的键盘有附件栏。