Textarea
Displays a form textarea.
src/components/rahti_ui/textarea.rs
Basics
`rows` sets where the box opens; `field-sizing-content` in the class list grows it past that as soon as there is enough text. `min-h-16` is the floor either way.
Initial value
The one place a textarea reads differently from an input: its value is its children, placed with `<slot />`. A textarea's content is character data, so what a user typed round-trips as text rather than becoming markup.
States
A `false` leaves the attribute off rather than writing `disabled="false"`, which HTML would read as disabled. `.invalid(true)` writes `aria-invalid`, and the `aria-invalid:` utilities in the class list do the rest.
With a label
`id` and `name` are named props for the same reason `placeholder` is: they are what a form is actually written with. Everything rarer goes through `attrs`.
Nothing here is submitted anywhere.
Attributes
`.attr(…)` writes anything; `.unset(…)` takes one away, which is the thing a value cannot express and the reason a forwarded set is an `Attrs` rather than a list of pairs. The last field has no `data-slot`.
Class override
`class` is merged, not appended: a utility written here replaces the one it conflicts with and leaves the rest of the field alone — `min-h-40` takes `min-h-16`'s place rather than fighting it. The merge is `twMerge`'s, in the browser, so unlike every other field on this page these three are unstyled until PulsePoint mounts.
Controlled, page scope
A field whose value is this page's state belongs on markup this page owns. `textarea_variants` returns the same class list the component renders, so a native `<textarea>` here is a Textarea in every way that shows. Note `field-sizing-content`: the box grows as you type, with `min-h-16` as its floor.
{draft.length} of 280 characters, {draft.split("\n").length} line(s).
Validation, inline
`.render(children)` returns the element itself, with no boundary around it, so interpolating it into this block puts it in this block's scope. Handlers and bindings both work, and it is still the same component. `aria-invalid` is bound here, which is what paints the field — the accessible state and the styling are one fact.
`value` is bound too, which is what makes this field controlled: the state it writes on every keystroke is the state it reads back. A textarea nothing is bound to keeps what is typed into it and is left alone by a render — that took a runtime fix, since a textarea's value is its children and every render produces those.
{bio.length > 280 ? "Too long by " + (bio.length - 280) + " — the field is marked invalid." : "Room for " + (280 - bio.length) + " more."}
The component tag, and its boundary
The tag is the right call for a field this page does not drive — a name, a placeholder, a `rows`, a `required`. What a plain form posts, the tag renders.
Where the tag stops
The same boundary an Input has, for the same reason. A click is resolved when it fires, late enough to reach this page — which is why `<Button onclick={…}>` drives a page's counter through the component. `value` and `oninput` are not: PulsePoint owns them, and takes that ownership in the scope the element is mounted in, which through a tag is the component's.
// Compiles against a scope with no `draft`, and does nothing.
<Textarea value={draft} oninput={setDraft(target.value)} />It is left out of this page rather than demonstrated: the binding throws a `ReferenceError` into `.rahti/dev.log` on every load. The two working spellings are the two sections above.