Repository navigation
Forms and Text Input
Input and Textarea are editable text fields with the behavior of a
browser's: typing, arrows and Ctrl+arrows, Home and End, Backspace and Delete,
Ctrl+A, undo and redo (Ctrl+Z, Ctrl+Y), the clipboard, mouse selection (click,
drag, double and triple click) and input methods (IME) for Japanese, Chinese
and Korean (on Wayland and Windows; X11 has no IME yet). Form groups fields and submits on Enter, and Label names a
field and focuses it when clicked.
Fields have Tailwind's Preflight defaults: no border, padding or background (style them with classes), the inherited font, and a width of about 20 characters unless a class sets one. They take no children.
A controlled field shows a value from your state, as in React. Bind is the
shortcut for the usual pair, Value(text) plus an OnChange that calls the
setter:
func Signup() tela.Node {
email, setEmail := tela.UseState("")
submitted, setSubmitted := tela.UseState("")
return Form(Class("flex flex-col gap-2"),
OnSubmit(func(tela.SubmitEvent) { setSubmitted.Set(email) }),
Label(For("email"), Class("text-sm font-medium"), Text("Email")),
Input(ID("email"), Type("email"), Placeholder("you@example.com"),
Bind(email, setEmail),
Class("rounded-md border border-slate-300 px-3 py-2 "+
"placeholder:text-slate-400 invalid:border-red-500")),
Button(Class("rounded-md bg-indigo-600 px-4 py-2 text-white"), Text("Sign up")),
If(submitted != "", P(Textf("Welcome, %s", submitted))),
)
}Written out, the same field is
Input(Value(email), OnChange(func(e tela.ChangeEvent) { setEmail.Set(e.Value) })).
Bind also takes a func(string) in place of the setter, so a reducer's
Dispatcher can bind: Bind(form.Email, func(s string) { d.Dispatch(setEmail(s)) }).
After every event the field shows Value again unless your handler changed it,
which is also how you restrict input.
DefaultValue gives the field an initial value and then leaves it alone.
Read it when you need it with ElementRef.Value:
func Quick() tela.Node {
input := tela.UseElementRef()
return Div(
Input(Ref(input), DefaultValue("hello"), AriaLabel("Message")),
Button(OnClick(func(tela.MouseEvent) { send(input.Value()) }), Text("Send")),
)
}
func send(string) {}BindValue binds a field to a *tela.Value[string] from
tela.UseValue: typing writes the value without rebuilding the component
that owns it (only what reads the value rebuilds or repaints), and the field
follows v.Set from anywhere.
func Filter() tela.Node {
query := tela.UseValue("")
return Div(
Input(Type("search"), BindValue(query), AriaLabel("Filter")),
Span(TextFn(func() string { return "Searching for " + query.Get() })),
)
}| Prop | Does |
|---|---|
Type |
"text" (default), "password" (dots; cannot be copied), "email", "search", "url", "tel", "number" (hints for input methods; the value stays text). Invalid email and url values match invalid: (not in a ReadOnly or disabled field, which HTML bars from validation). On a Button: "submit" (the default in a form), "button", "reset"
|
Rows |
How many lines tall a Textarea is (2 by default) |
Placeholder |
Text shown while the field is empty; style it with placeholder: classes |
ReadOnly |
Focusable, selectable and copyable, but not editable; matches read-only:
|
MaxLength |
Limits typed and pasted text to n UTF-16 code units (HTML's maxlength) |
Name |
Names a field or button in its form |
AutoComplete, SpellCheck
|
Accepted for familiarity; Tela has no autofill or spell checker |
A Textarea also wraps its text, scrolls when the text is taller than it, and
Enter inserts a line break; with text-nowrap (or whitespace-pre) its lines
do not wrap and it scrolls horizontally too. With field-sizing-content it
grows with its text between min-h-* and max-h-*:
func Message() tela.Node {
text, setText := tela.UseState("")
return Textarea(Rows(3), Placeholder("Write a message"), Bind(text, setText),
Class("field-sizing-content min-h-20 max-h-48 w-full rounded-md border p-2"))
}Form submits on Enter in one of its single-line fields, or on a click on a
Button inside it (buttons in a form are submit buttons unless Type("button"));
OnSubmit receives a tela.SubmitEvent. Tela does not navigate; PreventDefault
exists for familiarity. A button with Type("reset") restores the
DefaultValue of the uncontrolled fields.
Label gives a field its accessible name and focuses it when clicked. It
labels the element whose ID is its For, or else the field inside it:
func Labels() tela.Node {
return Div(
Label(For("name"), Text("Name")), Input(ID("name")),
Label(Text("Nickname "), Input()),
)
}Use tela.UseID to generate the IDs of a reusable field; see
Hooks. A Label that labels nothing (its For names
no mounted Input, Textarea or Button, and none is inside it) names and
focuses nothing, so dev mode warns about it once; for a caption that labels
no field, use Span or P.
| Handler | Event | When |
|---|---|---|
OnChange |
tela.ChangeEvent |
After every edit (React's onChange, per keystroke): Value, InputType, Data, IsComposing
|
OnBeforeInput |
tela.InputEvent |
Before an edit, cancelable with PreventDefault (not during an IME composition) |
OnCompositionStart, OnCompositionUpdate, OnCompositionEnd
|
tela.CompositionEvent |
An input method starts, updates and ends composing; Data is the text |
OnSelect |
tela.SelectEvent |
The selection changed: Start, End in UTF-8 byte offsets |
OnSubmit |
tela.SubmitEvent |
The form was submitted |
All of them bubble, so a Form's OnChange sees the edits of all its fields.
InputType uses the W3C names ("insertText", "insertFromPaste",
"deleteContentBackward", "historyUndo"...). To accept digits only:
func Digits() tela.Node {
return Input(AriaLabel("Code"), OnBeforeInput(func(e tela.InputEvent) {
if strings.Trim(e.Data, "0123456789") != "" {
e.PreventDefault()
}
}))
}With Ref and an tela.ElementRef, a field can be read and selected from
code. Offsets are UTF-8 byte offsets of the value:
-
ElementRef.Valuereturns the value (the DOM'sinput.value). -
ElementRef.Selectselects all of it. -
ElementRef.SetSelectionRangeselects part of it andElementRef.SelectionRangereturns the selection. -
ElementRef.Focusfocuses the field.
func Search() tela.Node {
search := tela.UseElementRef()
return Div(OnKeyDown(func(e tela.KeyboardEvent) {
if e.Key == "/" && !search.Focused() {
e.PreventDefault()
search.Focus()
search.Select()
}
}),
Input(Ref(search), Type("search"), Placeholder("Press / to search"),
Class("rounded-md border px-2 py-1")),
)
}App.Fill replaces a field's value like Playwright's fill;
App.Type sends one key at a time through the real key path; and
App.ExpectValue waits for a value. See Testing.
The examples/login screen puts all of this together in its Create account
mode: kit.Field around every control with an aria-invalid e-mail, a
password with a show/hide toggle and a strength meter, an auto-growing
textarea with a live counter, and a submit button that stays disabled until
the form is valid. Its Sign in mode shows the errors after the first try and a
request in flight.
