自定义输入

构建您的第一个自定义输入?阅读指南

FormKit 内置了许多输入类型,但您也可以定义自己的输入,这些输入会自动继承 FormKit 的增值功能,如验证、错误信息、数据建模、分组、标签、帮助文本等。

修改或重构现有输入

如果您的用例需要修改现有输入,例如移动部分、更改或重构 HTML 元素等,请考虑使用 FormKit 的输入导出功能

输入由两个基本部分组成:

  1. 输入定义
  2. 输入的代码:一个架构或一个组件
从指南开始

如果您刚开始接触自定义输入,请考虑阅读“创建自定义输入”指南。本页内容旨在解释自定义输入的复杂性,适用于编写插件或库等高级用例,并非许多常见用例所必需。

注册输入

新的输入需要一个输入定义。输入定义可以通过三种方式在 FormKit 中注册:

输入定义

输入定义是包含初始化输入所需信息的对象——比如接受哪些属性、渲染哪个架构或组件,以及是否包含任何额外的功能函数。定义对象的结构如下:

{
  // 节点类型:input、group 或 list。
  type: 'input',
  // 要渲染的架构(架构对象或返回对象的函数)
  schema: [],
  // 要渲染的 Vue 组件(使用 schema _或_ 组件,但不要同时使用)
  component: YourComponent,
  // (可选)<FormKit> 组件应接受的特定于输入的属性。
  // 应为驼峰式字符串数组
  props: ['fooBar'],
  // (可选)接收节点的函数数组。
  features: []
}

使用 type 属性

让我们创建一个最简单的输入 —— 一个只输出 "Hello world" 的输入。

加载实时示例

尽管这个简化的例子没有包含任何输入/输出机制,它仍然符合完整输入的标准。它可以有一个值,运行验证规则(它们不会显示,但它们可以阻止表单提交),并执行插件。从根本上说,所有输入都是核心节点,输入的定义提供了与该节点交互的机制。

全局自定义输入

要在应用程序中通过 "type" 字符串(例如:<FormKit type="foobar" />)使用您的自定义输入,您可以向 defaultConfig 选项中添加一个 inputs 属性。inputs 对象的属性名称将成为应用程序中 <FormKit> 组件可用的 "type" 字符串。

import { createApp } from 'vue'
import App from 'App.vue'
import { plugin, defaultConfig } from '@formkit/vue'

const helloWorld = {
  type: 'input',
  schema: ['Hello world'],
}

createApp(App)
  .use(
    plugin,
    defaultConfig({
      inputs: {
        // 属性将是 <FormKit type="hello"> 中的 “type”
        hello: helloWorld,
      },
    })
  )
  .mount('#app')

现在我们已经定义了我们的输入,我们可以在应用程序中任何地方使用它:

加载实时示例

插件库

上面的例子扩展了 @formkit/inputs 库(通过 defaultConfig)。然而,FormKit 的一个强大特性是它能够从多个插件加载输入库。然后,这些输入可以在任何可以定义插件的地方注册:

  • 全局
  • 每个组
  • 每个表单
  • 每个列表
  • 每个输入

让我们将我们的 hello world 输入重构为使用它自己的插件:

加载实时示例
插件继承

请注意,在上面的例子中,我们的插件是在实际使用它的元素的父级上定义的!这要归功于插件继承 —— FormKit 插件的核心特性。

Schema 与组件

您的输入可以使用 FormKit 的 schema 或一个通用的 Vue 组件来编写。每种方法都有其优缺点:

代码优点缺点
Vue
  • 学习曲线(您可能知道如何编写 Vue 组件)。
  • 更成熟的开发工具。
  • 稍微更快的初始渲染。
  • 不能使用 :sections-schema 属性 来修改结构。
  • 插件不能修改 schema 来改变渲染输出。
  • 框架特定(仅限 Vue)。
  • 容易编写不适合 FormKit 生态系统的输入。
Schema
  • 结构可以通过 :sections-schema 属性修改(如果您允许)。
  • 插件可以修改/改变渲染输出。
  • 框架无关(未来支持 FormKit 支持新框架的可移植性)。
  • 生态系统兼容性(非常适合发布您自己的开源输入)。
  • 学习曲线(需要 理解 schemas)。
  • 稍微更慢的初始渲染。
  • 开发工具不够成熟。
在 schemas 中的组件

即使您更喜欢使用标准 Vue 组件来编写自定义输入,您仍然可以在输入定义中使用 schema。请阅读使用 createInput 来扩展基础 schema 部分。

主要的结论是,如果您计划在多个项目中使用自定义输入 —— 那么考虑使用基于 schema 的方法。如果您的自定义输入只会在单个项目中使用,并且灵活性不是一个问题,使用 Vue 组件。

未来防护

将来,FormKit 可能会扩展以支持其他框架(例如:React 或 Svelte。如果您对此感兴趣,请告诉我们!)。使用 schema 编写您的输入意味着您的输入也将兼容这些框架(可能需要最小的更改)。

Schema 输入

FormKit 的所有核心输入都是使用 schema 编写的,以提供尽可能最大的灵活性。编写您自己的 schema 输入时,您有两个主要选项:

了解“标准”FormKit 输入的基本结构是很重要的,它被分解成部分

电子邮件地址
🤟
test@example.com
🚀
请使用您的学校电子邮件地址。
请提供一个有效的电子邮件。
标准 FormKit 文本输入的组成。

上图中的 input 部分通常是您在创建自己的输入时会替换的部分 —— 保持包装器、标签、帮助文本和消息不变。然而,如果您也想控制这些方面,您也可以从头开始编写自己的输入。

使用 createInput 扩展基础 schema

要使用基础 schema 创建输入,您可以使用 @formkit/vue 包中的 createInput() 工具。这个函数接受 3 个参数:

  • (必需)一个 schema 节点 一个 Vue 组件,它将被插入到基础 schema 的 input 部分(见上图)。
  • (可选)一个要与自动生成的输入定义合并的输入定义属性对象。
  • (可选)一个部分-schema 对象(就像部分-schema 属性一样)与基础 schema 合并。这允许您修改输入的包装结构。

该函数返回一个可立即使用的输入定义

当提供一个 组件 作为第一个参数时,createInput 将生成一个在基础 schema 中引用您的组件的 schema 对象。您的组件将传递一个单一的 context 属性:

{
  $cmp: 'YourComponent',
  props: {
    context: '$node.context'
  }
}

当提供一个 schema 对象时,您的 schema 将直接注入到基础 schema 对象中。注意,我们的 hello world 示例现在支持输出“标准”的 FormKit 功能,如标签、帮助文本和验证:

加载实时示例

从零开始编写 schema 输入

有时候,完全从零开始编写输入而不使用任何基础 schema 功能是有意义的。这样做时,只需提供您的完整 schema 对象的输入定义

加载实时示例

在上面的例子中,我们能够重新创建 createInput 示例的相同功能 —— 即 —— 标签、帮助文本和验证消息输出。然而,我们仍然缺少一些“标准”的 FormKit 功能,如插槽支持。如果您试图发布您的输入或保持与其他 FormKit 输入的 API 兼容性,请查看输入清单

组件输入

自定义输入 vs Vue 组件包装器

在使用 Vue 组件编写自定义 FormKit 输入时,建议不要在内部使用 FormKit 组件,自定义输入应该像常规输入一样编写,但可以利用 FormKit 上下文属性来添加 FormKit 所需的功能。如果您的情况是要使用带有默认值的 FormKit 组件,建议改用 Vue 组件包装器并直接调用该组件,FormKit 输入可以在任何嵌套级别上工作,或者您也可以考虑使用 FormKit 的输入导出功能来添加功能以及更改属性和属性。

对于大多数用户来说,将 Vue 组件传递给 createInput 在定制化和增值功能之间提供了一个良好的平衡。如果您想要完全脱离基于 schema 的输入,您可以直接将一个组件传递给输入定义。

组件输入接收单个属性 —— 上下文对象。然后由您编写一个组件来包含 FormKit 的期望功能(标签、帮助文本、消息显示等)。查看输入清单以获取您将要输出的内容列表。

输入和输出值

输入有两个关键角色:

  • 接收用户输入。
  • 显示当前值。

接收输入

您可以通过任何用户交互接收输入,并且输入可以设置为任何类型的数据。输入不仅限于字符串和数字 —— 它们可以愉快地存储数组、对象或自定义数据结构。

从根本上说,输入所需要做的就是用一个值调用 node.input(value)node.input() 方法是自动防抖的,所以请随意频繁调用 —— 比如每次敲击键盘。通常,这看起来像是绑定到 input 事件。

上下文 对象 包括一个基本输入类型的输入处理器:context.handlers.DOMInput。这可以用于文本类输入,其中输入的值可在 event.target.value 处获得。如果您需要更复杂的事件处理器,您可以使用“特性”来暴露它

任何用户交互都可以被视为输入事件。对于许多原生 HTML 输入,该交互是通过 input 事件 捕获的。

// 用 schema 编写的 HTML 文本输入:
{
  $el: 'input',
  attrs: {
    onInput: '$handlers.DOMInput'
  }
}

在 Vue 模板中的等效写法:

<template>
  <input @input="context.DOMInput" />
</template>

显示值

输入组件同样负责显示当前值。通常,你会想使用 node._value$_value 在 schema 中来显示一个值。这是“实时”的非防抖值。当前的 已提交 值是 node.value$value)。阅读更多关于“值的确定”这里

// 一个在 schema 中编写的 HTML 文本输入:
{
  $el: 'input',
  attrs: {
    onInput: '$handlers.DOMInput',
    value: '$_value'
  }
}

在 Vue 模板中的等效写法:

<template>
  <input :value="context._value" @input="context.handlers.DOMInput" />
</template>
_value 与 value

未提交输入 _value 应该使用的唯一时间是在输入本身上显示值 — 在所有其他位置,使用已提交的 value 是很重要的。

添加属性

你可以将 标准 FormKit 属性(如 labeltype)传递给 <FormKit> 组件,这些属性在 context 对象的根部和 core node props 中可用,你可以在你的 schema 中直接引用这些属性表达式(例如:$label)。传递给 <FormKit> 组件的任何非 节点属性 都会出现在 context.attrs 对象中(在 schema 中就是 $attrs)。

如果你需要额外的属性,你可以在输入定义中声明它们。属性也可以用来接受来自 <FormKit> 组件的新属性,但它们也用于内部输入状态(很像 Vue 3 组件中的 ref)。

FormKit 使用 props 命名空间来处理这两种用途(请参阅下面的自动完成示例)。属性应 始终 以 camelCase 定义,并在你的 Vue 模板中以 kebab-case 使用。有 2 种定义属性的方法:

  1. 数组表示法
  2. 对象表示法
  3. node.addProps() 方法

数组表示法

加载实时示例

使用 createInput 帮助器扩展基本 schema 时,传递一个带有输入定义值的第二个参数进行合并:

加载实时示例

对象表示法

对象表示法通过让你能够精细控制如何定义你的属性,给了你这样的能力:

  • 定义一个默认值。
  • 定义可以不带值传递的 boolean 属性。
  • 定义自定义的 getter/setter 函数。
加载实时示例

添加属性方法 (node.addProps())

您可以在任何可以访问节点的运行时环境中使用 node.addProps() 方法动态添加属性。对于自定义输入,这在特性中使用时特别有帮助。支持数组表示法和对象表示法(见上文)。

加载实时示例

添加特性

特性是向自定义输入类型添加功能的首选方式。一个“特性”仅仅是一个函数,它接收 核心节点 作为参数。实际上,它们是没有继承的插件(所以它们只适用于当前节点)。您可以使用特性来添加输入处理器、操作值、与属性交互、监听事件等等。

特性在数组中定义,以鼓励在可能的情况下重用代码。例如,我们在 selectcheckboxradio 输入上使用了一个名为“options”的特性

作为一个例子,假设您想构建一个输入,允许用户输入两个数字,输入的值是这两个数字的和:

加载实时示例

TypeScript 支持

FormKit 是用 TypeScript 编写的,并包含了其所有核心输入的类型定义。如果您正在编写自己的输入并希望提供 TypeScript 支持,您可以使用两个模块增强来定义自己的输入:

添加属性类型

<FormKit> 组件的 type 属性是一个字符串,用作属性的鉴别联合(FormKitInputProps)的键。通过增强这个类型,您的自定义输入可以定义它们自己的属性类型。要做到这一点,您必须增强 FormKitInputProps 类型以添加您自己的自定义类型:

declare module '@formkit/inputs' {
  interface FormKitInputProps<Props extends FormKitInputs<Props>> {
    // 这个键和 `type` 应该匹配:
    'my-input': {
      // 定义您的输入 `type`:
      type: 'my-input',
      // 定义一个可选属性。所有属性名称使用驼峰式命名:
      myOptionalProp?: string | number
      // 定义一个必需的属性
      superImportantProp: number
      // 定义值类型,这应该始终是可选的!
      value?: string | number
      // 使用 Prop 泛型从另一个字段推断信息,注意
      // 我们使用一个实用工具 "PropType" 来从 Props
      // 泛型推断 `value` 的类型:
      someOtherProp?: PropType<Props, 'value'>
    }
  }
}

添加插槽类型

如果您在自定义输入中定义了自己的部分(插槽),您也可以为这些部分添加TypeScript支持。要做到这一点,您必须扩展FormKitInputSlots类型以添加您自己的自定义插槽:

declare module '@formkit/inputs' {
  interface FormKitInputProps<Props extends FormKitInputs<Props>> {
    'my-input' {
      type: 'my-input'
      // ... 在这里添加属性
    }
  }

  interface FormKitInputSlots<Props extends FormKitInputs<Props>> {
    'my-input': FormKitBaseSlots<Props>
  }
}

在上面的例子中,我们使用了FormKitBaseSlots — 一个TypeScript工具,用于添加大多数自定义输入实现的“基本”插槽,如outerlabelhelpmessage等。然而,您也可以完全从头开始定义自己的插槽,或者扩展FormKitBaseSlots以添加额外的插槽(FormKitBaseSlots<Props> & YourCustomSlots)。

declare module '@formkit/inputs' {
  // ... 在这里添加属性
  interface FormKitInputSlots<Props extends FormKitInputs<Props>> {
    'my-input': {
      // 这将是my-input输入上*唯一*可用的插槽
      slotName: FormKitFrameworkContext & {
          // 这将作为`slotName`插槽中的插槽数据可用
          fooBar: string
        }
      }
    }
  }
}
首先扩展属性

为了扩展FormKitInputSlots,您必须首先编写一个FormKitInputProps的扩展,至少包括type属性。

示例

以下是一些自定义输入的示例。它们并不意味着全面或适合生产环境,而是用来说明一些自定义输入功能。

简单文本输入

这是最简单的可能输入,它不利用FormKit内置的任何DOM结构,只输出一个文本输入 — 然而它是其嵌套组内的一个完全功能的成员,并能够读取和写入值。

加载实时示例
DOM 输入

在上面的示例中,$handlers.DOMInput 是一个内置的便利函数,用于 (event) => node.input(event.target.value)

自动完成输入

让我们来看一个稍微复杂一些的示例,它利用createInput提供所有标准的FormKit结构,同时仍然提供自定义输入界面。

加载实时示例

输入清单

FormKit 为即使是最普通的输入暴露了数十个增值特性。当为特定项目编写自定义输入时,你只需要实现实际将在该项目上使用的特性。然而,如果你计划将你的输入分发给其他人,你会希望确保这些特性是可用的。例如,标准的 <FormKit type="text"> 输入使用以下模式为其 input 元素:

{
  $el: 'input',
  bind: '$attrs',
  attrs: {
    type: '$type',
    disabled: '$disabled',
    class: '$classes.input',
    name: '$node.name',
    onInput: '$handlers.DOMInput',
    onBlur: '$handlers.blur',
    value: '$_value',
    id: '$id',
  }
}

在上面的模式中有几个特性可能不是立即显而易见的,比如 onBlur 处理器。以下清单旨在帮助输入作者覆盖所有基础:

How is your input built?
  • The outermost wrapper element on all FormKit inputs.
  • The value of the label prop must be displayed and linked for accessibility with the for attribute.
  • Users can override the label slot.
  • Users can extend the label section using the label section key.
  • The value of the help prop must be displayed.
  • Users can override the help slot.
  • Users can extend the help section using the help section key.
  • Each message in the context.messages object must displayed if it is set to visible.
  • Users can override the messages slot.
  • Users can extend the messages section using the messages section key.
  • Users can override the message slot.
  • Users can extend the message section using the message section key..
  • Users can override the input slot.
  • The primary input element should include an id attribute (context.id).
  • The primary input element should include a name attribute (context.node.name).
  • The primary input element should call context.handlers.blur when blurred.
  • The primary input element should call node.input(value) when the user provides input. You can use context.handlers.DOMInput for text-like inputs.
  • The primary input element should display the current value of the input using context._value.
  • The primary input element should apply the disabled attribute when context.disabled is true.
  • All events bindings should be passed through. Use bind: '$attrs' in schemas.
  • Classes for all DOM elements should be applied using context.classes.{section-key}.