Constructs

Constructs 让你以声明式的、类似 React 的方式组合 UI。它们是创建 VNodes(虚拟节点)的工厂函数。VNode 是组件的轻量级描述。当你将 VNode 添加到树中时,它会变成一个真正的 Renderable。

基本用法

Constructs 看起来像是返回组件描述的函数调用:

import { createCliRenderer, Box, Text, Input } from "@opentui/core"

const renderer = await createCliRenderer()

renderer.root.add(
  Box(
    { width: 40, height: 10, borderStyle: "rounded", padding: 1 },
    Text({ content: "Welcome!" }),
    Input({ placeholder: "Enter your name..." }),
  ),
)

将子元素作为属性对象之后的额外参数传递,而不是作为属性。

可用的 Constructs

大多数组核心 Renderable 类都有对应的 construct 函数:

import {
  ASCIIFont,
  Box,
  Code,
  FrameBuffer,
  Input,
  ScrollBox,
  Select,
  SyntaxStyle,
  TabSelect,
  Text,
  RGBA,
} from "@opentui/core"

// 这些创建的是 VNodes,而不是真正的 Renderables
const syntaxStyle = SyntaxStyle.fromStyles({
  default: { fg: RGBA.fromHex("#FFFFFF") },
})
const box = Box({ border: true })
const text = Text({ content: "Hello" })
const input = Input({ placeholder: "Type here..." })
const code = Code({ content: "const x = 1", filetype: "typescript", syntaxStyle })
const scrollBox = ScrollBox({ width: 40, height: 10 })
const frameBuffer = FrameBuffer({ width: 20, height: 10 })
const ascii = ASCIIFont({ text: "OPEN", font: "tiny" })

Constructs 的工作原理

当你调用 Box() 这样的 construct 时,它会创建一个 VNode —— 一个描述要创建什么的普通对象:

// 这创建的是 VNode,而不是真正的 BoxRenderable
const myBox = Box({ width: 20, height: 10 })

// VNode 在添加到树时才会被实例化
renderer.root.add(myBox) // 现在它变成了真正的 BoxRenderable

这种延迟创建让你可以:

  • 在没有渲染上下文的情况下组合 UI
  • 在组件存在之前排队方法调用
  • 编写更清晰、更具声明性的代码

创建自定义 Constructs

通过编写返回 VNodes 的函数来创建可复用组件:

function LabeledInput(props: { label: string; placeholder: string }) {
  return Box(
    { flexDirection: "row", gap: 1 },
    Text({ content: props.label }),
    Input({ placeholder: props.placeholder, width: 20 }),
  )
}

renderer.root.add(
  Box(
    { flexDirection: "column", padding: 1 },
    LabeledInput({ label: "Name:", placeholder: "Enter name..." }),
    LabeledInput({ label: "Email:", placeholder: "Enter email..." }),
  ),
)

VNodes 上的方法链

VNodes 支持方法调用。系统会排队这些调用并在创建组件后应用它们:

const input = Input({ id: "my-input", placeholder: "Type here..." })

// VNode 会排队这个调用
input.focus()

// 当添加到树时,系统会创建 input 并调用 focus()
renderer.root.add(input)

你也可以设置属性:

const box = Box({ id: "my-box" })
box.backgroundColor = RGBA.fromHex("#333366")
renderer.root.add(box)

delegate() 函数

组合组件通常需要将外部方法调用路由到特定的内部组件。delegate() 函数将方法和属性名称映射到后代 ID:

import { delegate, Box, Text, Input } from "@opentui/core"

function LabeledInput(props: { id: string; label: string; placeholder: string }) {
  return delegate(
    {
      focus: `${props.id}-input`, // 将 focus() 路由到 input
      value: `${props.id}-input`, // 将 value 属性访问路由到 input
    },
    Box(
      { flexDirection: "row" },
      Text({ content: props.label }),
      Input({
        id: `${props.id}-input`,
        placeholder: props.placeholder,
        width: 20,
      }),
    ),
  )
}

const username = LabeledInput({ id: "username", label: "Username:", placeholder: "Enter username..." })

// 这实际上会让嵌套的 Input 获得焦点,而不是外层的 Box
username.focus()

renderer.root.add(username)

委托映射

映射对象的键是方法或属性名称,值是后代 ID:

delegate(
  {
    focus: "inner-input", // .focus() -> 查找后代 "inner-input" 并调用 focus()
    blur: "inner-input", // .blur() -> 同上
    add: "content-area", // .add() -> 将子元素添加到 "content-area"
    value: "inner-input", // .value 的 get/set -> 代理到 "inner-input"
  },
  vnode,
)

使用子元素组合

自定义 constructs 可以接受并传递子元素:

function Card(props: { title: string }, ...children: VChild[]) {
  return Box(
    { border: true, padding: 1, flexDirection: "column" },
    Text({ content: props.title, fg: "#FFFF00" }),
    Box({ flexDirection: "column" }, ...children),
  )
}

renderer.root.add(Card({ title: "User Info" }, Text({ content: "Name: Alice" }), Text({ content: "Role: Admin" })))

混合使用 Renderables 和 Constructs

你可以混合使用两种方式:

import { BoxRenderable, Text, Input } from "@opentui/core"

// 直接创建 Renderable
const container = new BoxRenderable(renderer, {
  id: "container",
  flexDirection: "column",
})

// 向其添加 Constructs
container.add(Text({ content: "Title" }), Input({ placeholder: "Type here..." }))

renderer.root.add(container)

或者使用包含 Renderables 的 constructs:

const customRenderable = new CustomRenderable(renderer, { id: "custom" })

renderer.root.add(
  Box(
    { padding: 1 },
    Text({ content: "Header" }),
    customRenderable, // 混入的普通 Renderable
    Text({ content: "Footer" }),
  ),
)

下一步

请参阅 Renderables 与 Constructs 了解何时使用每种方式的详细比较。