At the heart of the FormKit framework is @formkit/core
. This zero-dependency package is responsible for nearly all of FormKit's low-level critical functions, such as:
The functionality of FormKit core is not exposed to your application via a centralized instance but rather a distributed set of "nodes" (FormKitNode
) where each node represents a single input.
This mirrors HTML — in fact DOM structure is actually a general tree and FormKit core nodes reflect this structure. For example, a simple login form could be drawn as the following tree graph:
In this diagram, a form
node is a parent to three child nodes — email
, password
and submit
. Each input component in the graph "owns" a FormKit core node, and each node contains its own options, configuration, props, events, plugins, lifecycle hooks, etc. This architecture ensures that FormKit’s primary features are decoupled from the rendering framework (Vue) — a key to reducing side effects and maintaining blazing fast performance.
Additionally, this decentralized architecture allows for tremendous flexibility. For example — one form could use different plugins than other forms in the same app, a group input could modify the configuration of its sub-inputs, and validation rules can even be written to use props from another input.
Every <FormKit>
component owns a single core node, and each node must be one of three types:
input
), and their input type (like checkbox
).
Most of FormKit’s native inputs have a node type of input
— they operate on a single value. The value itself can be of any type, such as objects, arrays, strings, and numbers — any value is acceptable. However, nodes of type input
are always leafs — meaning they cannot have children.
A list is a node that produces an array value. Children of a list node produce a value in the list’s array value. The names of immediate children are ignored — instead each is assigned an index in the list’s array.
A group is a node that produces an object value. Children of a group node use their name
to produce a property of the same name in the groups’s value object — <FormKit type="form">
is an instance of a group.
In addition to specifying the type
of node when calling createNode()
, you can pass any of the following options:
Options | Default | Description |
---|---|---|
children | [] | Child FormKitNode instances. |
config | {} | Configuration options. These become the defaults of the props object. |
name | {type}_{n} | The name of the node/input. |
parent | null | The parent FormKitNode instance. |
plugins | [] | An array of plugin functions. |
props | {} | An object of key/value pairs that represent the current node instance details. |
type | input | The type of FormKitNode to create (list , group , or input ). |
value | undefined | The initial value of the input. |
FormKit uses an inheritance-based configuration system. Any values declared in the config
option are automatically passed to children (and all descendants) of that node, but not passed to siblings or parents. Each node can override its inherited values by providing its own config, and these values will in turn be inherited by any deeper children and descendants. For example:
The above code will result in each node having the following configuration:
node.props
rather than node.config
. The next section details this feature.
The node.props
and node.config
objects are closely related. node.config
is best thought of as the initial values for node.props
. props
is an arbitrarily shaped object that contains details about the current instance of the node.
The best practice is to always read configuration and prop data from node.props
even if the original value is defined using node.config
. Explicitly defined props take precedence over configuration options.
<FormKit>
component, any props defined for the input type
are automatically set as node.props
properties. For example: <FormKit label="Email" />
would result in node.props.label
being Email
.
You can set the initial value of a node by providing the value
option on createNode()
— but FormKit is all about interactivity, so how do we update the value of an already defined node? By using node.input(value)
.
In the above example username.value
is still undefined immediately after it’s set because node.input()
is asynchronous. If you need to read the resulting value after calling node.input()
you can await the returned promise.
Because node.input()
is asynchronous, the rest of our form does not need to recompute its dependencies on every keystroke. It also provides an opportunity to perform modifications to the unsettled value before it is "committed" to the rest of the form. However — for internal node use only — a _value
property containing the unsettled value of the input is also available.
node.value = 'foo'
. Instead, you should always use node.input(value)
Now that we understand node.input()
is asynchronous, let's explore how FormKit solves the "settled tree" problem. Imagine a user quickly types in their email address and hits "enter" very quickly — thus submitting the form. Since node.input()
is asynchronous, incomplete data would likely be submitted. We need a mechanism to know when the whole form has "settled".
To solve this, FormKit’s nodes automatically track tree, subtree, and node "disturbance". This means the form (usually the root node) always knows the settlement state of all the inputs it contains.
The following graph illustrates this "disturbance counting". Click on any input node (blue) to simulate calling node.input()
and notice how the whole form is always aware of how many nodes are "disturbed" at any given time. When the root node has a disturbed count of 0
the form is settled and safe to submit.
<FormKit type="form">
input already incorporates this await behavior. It will not call your @submit
handler until your form is completely settled. However when building advanced inputs it can be useful to understand these underlying principles.
Sometimes it can be helpful to get the underlying instance of a node from the Vue <FormKit>
component. There are two primary methods of fetching an input’s node.
$formkit.get()
(or getNode()
for composition API)@node
event.this.$formkit.get()
When using FormKit with the Vue plugin (recommended), you can access a node by assigning it an id
and then accessing it by that property.
id
to use this method.
this
within setup
. You can access the same getNode()
function by importing it from @formkit/core
.import { getNode } from '@formkit/core'
Another way to get the underlying node
is to listen to the @node
event which is emitted only once when the component first initializes the node.
To traverse nodes within a group or list use node.at(address)
— where address
is the name
of the node being accessed (or the relative path to the name). For example:
If the starting node has siblings, it will attempt to locate a match in the siblings (internally, this is what FormKit uses for validation rules like confirm:address
).
You can go deeper than one level by using a dot-syntax relative path. Here's a more complex example:
Notice how traversing the list
uses numeric keys, this is because the list
type uses array indexes automatically.
node.at('foo.bar')
could be expressed as node.at(['foo', 'bar'])
.
Also available for use in node.at()
are a few special "tokens":
Token | Description |
---|---|
$parent | The immediate ancestor of the current node. |
$root | The root node of the tree (the first node with no parent). |
$self | The current node in the traversal. |
find() | A function that performs a breadth-first search for a matching value and property. For example: node.at('$root.find(555, value)') |
These tokens are used in dot-syntax addresses just like you would use a node’s name:
Nodes have their own events which are emitted during the node’s lifecycle (unrelated to Vue’s events).
To observe a given event, use node.on()
.
Event handler callbacks all receive a single argument of type FormKitEvent
, the object shape is:
Node events (by default) bubble up the node tree, but node.on()
will only respond to events emitted by the same node. However, if you would like to also catch events bubbling up from descendants you may append the string .deep
to the end of your event name:
Every call to register an observer with node.on()
returns a “receipt” — a randomly generated key — that can be used later to stop observing that event (similar to setTimeout()
and clearTimeout()
) using node.off(receipt)
.
The following is a comprehensive list of all events emitted by @formkit/core
. Third-party code may emit additional events not included here.
Name | Payload | Bubbles | Description |
---|---|---|---|
commit | any | yes | Emitted when a node's value is committed but before it has been transmitted to the rest of the form. |
config:{property} | any (the value) | yes | Emitted any time a specific configuration option is set or changed. |
child | FormKitNode | yes | Emitted when a new child node is added, created or assigned to a parent. |
created | FormKitNode | yes | Emitted immediately before the node is returned when calling createNode() (plugins and features have already run). |
defined | FormKitTypeDefinition | yes | Emitted when the node’s "type" is defined, this typically happens during createNode() . |
destroying | FormKitNode | yes | Emitted when the node.destroy() is called, after it has been detached from any parents. |
input | any (the value) | yes | Emitted when node.input() is called — after the input hook has run. |
message-added | FormKitMessage | yes | Emitted when a new node.store message was added. |
message-removed | FormKitMessage | yes | Emitted when a node.store message was removed. |
message-updated | FormKitMessage | yes | Emitted when a node.store message was changed. |
prop:{propName} | any (the value) | yes | Emitted any time a specific prop is set or changed. |
prop | { prop: string, value: any } | yes | Emitted any time a prop is set or changed. |
text | string or FormKitTextFragment | no | Emitted after the text hook has run — typically when processing interface text that may have been translated. |
prop
and prop:{propName}
events, so long as they do not override that property in their own props
or config
objects.
Node events are emitted with node.emit()
. You can leverage this feature to emit your own synthetic events from your own plugins.
An optional third argument bubble
is also available. When set to false
, it prevents your event from bubbling up through the form tree.
Hooks are middleware dispatchers that are triggered during pre-defined lifecycle operations. These hooks allow external code to extend the internal functionality of @formkit/core
. The following table details all available hooks:
Hook | Value | Description |
---|---|---|
classes |
| Dispatched after all class operations have been run, before final conversion to a string. |
commit | any | Dispatched when setting the value of a node after the input and debounce of node.input() is called. |
error | string | Dispatched when processing a thrown error — errors are generally inputs, and the final output should be a string. |
init | FormKitNode | Dispatched after the node is initially created but before it is returned in createNode() . |
input | any | Dispatched synchronously on every input event (every keystroke) before commit . |
prop |
| Dispatched when any prop is being assigned. |
text | FormKitTextFragment | Dispatched when a FormKit-generated string needs to be displayed — allowing i18n or other plugins to intercept. |
To make use of these hooks, you must register hook middleware. A middleware is simply a function that accepts 2 arguments — the value of the hook and next
— a function that calls the next middleware in the stack and returns the value.
To register a middleware, pass it to the node.hook
you want to use:
Plugins are the primary mechanism for extending the functionality of FormKit. The concept is simple — a plugin is just a function that accepts a node. These functions are then automatically called when a node is created, or when the plugin is added to the node. Plugins work similar to configuration options — they are automatically inherited by children and descendants.
In the example above, the plugin is only defined on the parent, but the child also inherits the plugin. The function myPlugin
will be called twice — once for each node in the graph (which only has two in this example):
In addition to extending and modifying nodes, plugins serve one additional role — exposing input libraries. A “library” is a function assigned to the library
property of a plugin that accepts a node and determines whether it knows how to “define” that node. If it does, it calls node.define()
with an input definition.
For example, if we wanted to create a plugin that exposed a couple new inputs: italy
and france
we could write a plugin to do this:
Experienced developers will notice a few exciting properties of this plugin-library pattern:
node.define()
. Frequently, this is simply checking node.props.type
but you can define different inputs based on other conditions, like if a particular prop is set.Each node has its own data store. The objects in these stores are called "messages" and these messages are especially valuable for three primary use cases:
Each message (FormKitMessage
in TypeScript) in the store is an object with the following shape:
createMessage({})
can be imported from @formkit/core
to merge your message data with the above default values to create a new message object.
To add or update a message, use node.store.set(FormKitMessage)
. Messages are then made available on node.store.{messageKey}
@formkit/i18n
plugin is installed and a matching key is available in the active locale. Read the i18n docs.
One of the keys to FormKit’s performance is its ability to efficiently count messages matching a given criteria (in the store), and then keep a running tally of those messages as changes are made (including from child nodes). These counters are created using node.ledger
.
Let's say we want to count how many messages are currently being displayed. We could do this by counting messages with the visible
property set to true
.
Notice the second argument of node.ledger.count()
is a function. This function accepts a message as an argument and expects the return value to be a boolean, indicating whether that message should be counted or not. This allows you to craft arbitrary counters for any message type.
When using a counter on a group
or list
node, the counter will propagate down the tree summing the value of all messages passing the criteria function and then tracking that count for store changes.
blocking
which counts the blocking property of all messages. This is how FormKit forms know if all their children are "valid".