NeoForms
Table of Contents
NeoForms
NeoForms (yalms.ui.neoforms) builds interactive forms inside Neovim buffers. Rather than using a modal dialog or a custom UI widget, forms are rendered as editable Neovim buffers with labeled fields, validation, and submit callbacks. The module uses Snacks.win for window management, so forms inherit Snacks' window styling, positioning, and keybinding system.
Prerequisites
- Snacks.nvim must be installed.
Currently, text fields and option fields (radio buttons and checkboxes) are supported. Fields render inline in the buffer, with protected regions that prevent the user from editing labels, separators, and other non-input areas.
Creating a Form
Forms are created by calling NeoForm:new(spec, handler). The first argument is a NeoFormSpec table that defines the form's title, fields, and behavior. The second argument is a callback function that receives the form values when the user submits.
local NeoForm = require('yalms.ui.neoforms')
local function handle_submit(values)
print(vim.inspect(values))
end
local form = NeoForm:new({
title = "User Settings",
fields = {
{ key = "username", kind = "text", title = "Username" },
{ key = "theme", kind = "options", title = "Theme",
options = { {label = "Dark", value = false}, {label = "Light", value = false} } },
}
}, handle_submit)
form:run()
After calling form:run(), the form displays in a floating window. The submit callback is invoked asynchronously when the user presses the submit key (default <leader><CR> in normal mode). The callback receives a table mapping each field's key to its current value. The form window closes automatically on submit unless the callback reopens it.
NeoForm Specification
The NeoFormSpec table accepts several options that control the form's appearance and behavior. The title field sets the window title displayed in the border. The desc field, if provided, renders a description at the top of the form. The fields array is required and defines the input fields.
Additional options include keys for custom keybindings, help for virtual lines rendered at the buffer bottom, validator for form-level validation, on_change for a callback that fires after any field changes, and transform for modifying values before they reach the submit handler. The ft option sets the buffer filetype and defaults to "markdown".
Field Types
Text Fields
Text fields accept freeform input and are created by setting kind = "text" in the field specification. The field renders as an inline text area with a title label and a separator. If the lang option is set, the field wraps its content in a code fence (triple backticks for markdown, #+begin_src /=#+endsrc= for org mode). When lang is set, the field automatically becomes multi-line, allowing the user to enter text across multiple lines.
{ key = "bio", kind = "text", title = "Biography", lang = "text", default = "Enter text..." }
Text fields support a default value, a per-field validator function, a hidden option that hides the field content behind an overlay, and multiline for explicit multi-line mode. When hidden, the field still occupies space in the buffer but its content is not visible to the user.
Option Fields
Option fields present a set of selectable items rendered inline in the buffer. They are created with kind = "options" and an options array. Each option is a table with a label (the display text) and a value (a boolean indicating the initial checked state).
Set radio = true in the field specification for single-select behavior (radio buttons), where selecting one option automatically deselects the others. Leave radio as false (the default) for multi-select (checkboxes), where any number of options can be selected.
{ key = "theme", kind = "options", title = "Theme",
options = {
{ label = "Dark", value = false },
{ label = "Light", value = false },
},
radio = true,
}
In normal mode, press , (comma) with the cursor on an option to toggle its state. The field's on_change callback fires after each toggle, allowing you to react to selection changes in real time.
Custom Keys
You can register additional key bindings that have access to the form instance through the keys table in FormSpec. Each entry maps to a Snacks.win key specification, with the second element being a function that receives the form and window objects.
keys = {
["preview"] = { "<C-p>", function(form, win)
print(vim.inspect(form:get_value()))
end, { desc = "Preview values" } }
}
Fields can also register keys via their register_key callback. These keys are automatically merged into the form's key map, so you can extend field behavior without modifying the form specification.
NeoForm Methods
Once created, a form exposes several methods. The run(opts?) method displays the form in a floating window, accepting optional snacks.win.Config overrides. It returns immediately; the submit callback is invoked asynchronously when the user submits.
The get_value() method returns the current form values as a table keyed by each field's key. The get_win() method returns the snacks.win window object, or nil if the form is not currently displayed. The reset_help(help) method updates the virtual lines rendered at the bottom of the form. Setting keep_on_submit = true in the form spec prevents the form from closing automatically after submission.
Submit and Validation
When the user submits the form (default key: <leader><CR> in normal mode), the form first runs each field's validation function. If a field's validator returns an error string, that error is displayed and submission is cancelled. If all fields pass validation, the form-level validator function is called, if set. If it returns an error, submission is also cancelled.
If the transform function is provided in the form specification, it is called with the validated values and the form instance. The return value of transform is passed to the submit handler instead of the original values. Use this to coerce types, restructure the result, or perform any final processing.
API Reference
Developer Guide
Creating Custom Field Types
To create a custom field type, extend the FormField class:
local FormField = require('yalms.ui.neoforms.field')
local CustomField = FormField:extend()
function CustomField:new(spec)
FormField.new(self, spec)
self.render_fn = spec.render_fn
end
function CustomField:render(bufnr, row, opts)
-- Custom render logic
end
-- Register the field type
require('yalms.ui.neoforms').register_field('custom', CustomField)
Extending Form Behavior
You can subclass NeoForm to add custom behavior:
local NeoForm = require('yalms.ui.neoforms')
local CustomForm = NeoForm:extend()
function CustomForm:new(spec, handler)
NeoForm.new(self, spec, handler)
self.custom_state = {}
end
function CustomForm:before_submit(values)
-- Custom pre-submission logic
return values
end
Hooks
Register a on_change callback in the form spec to react to field value changes:
local form = NeoForm:new({
title = "Settings",
fields = {...},
on_change = function(values, form)
print("Form changed:", vim.inspect(values))
end,
}, handler)
The form also supports a validator function and a transform function (see Submit and Validation).