a text input form

build a small form with a name field, a live echo label, a validated field, and a numeric field

by the end of this guide you will have a working form panel with three fields: an uncontrolled name entry that keeps its own text, a controlled entry whose value is mirrored live into a label below it and validated as you type, and a numeric field clamped to a 0-100 range. each step introduces one idea so you can see exactly what each property contributes before the next one arrives.

start with an uncontrolled entry

an uncontrolled entry seeds itself once from DefaultText and then owns its own text. you do not track the value in a field - the entry holds it. add a Placeholder for the hint when it is empty.

using Goo;
using Sandbox.UI;

public class InputFormUI : GooPanel<Container>
{
    protected override Container Build() => new Container
    {
        Padding = 16,
        Width = 360,
        FlexDirection = FlexDirection.Column,
        Gap = 8,
        Children =
        {
            new Text( "name" ),
            new Goo.TextEntry { DefaultText = "type me", Placeholder = "search" },
        },
    };
}

FlexDirection defaults to Row, so a form stacking fields top to bottom needs FlexDirection.Column set explicitly - without it the label and the entry would sit side by side instead of one above the other.

press play and type in the field. the entry keeps the text on its own. nothing else in Build knows or cares about it yet.

add a controlled entry with a live label

a controlled entry binds Value to a field you own. OnChange fires on every keystroke and hands you the current string - update your field there, that's it. the handler is a TextEntry event, so firing it already schedules the next Build() once it returns; the echo label below reflects each character as you type without an explicit Rebuild() call (see build method for the full AutoRebuildOnEvents contract). OnSubmit fires the same way when the user presses enter.

add _value and _submitted fields, then extend Build:

public class InputFormUI : GooPanel<Container>
{
    string _value = "";
    string _submitted = "(nothing yet)";

    protected override Container Build() => new Container
    {
        Padding = 16,
        Width = 360,
        FlexDirection = FlexDirection.Column,
        Gap = 8,
        Children =
        {
            new Text( "name" ),
            new Goo.TextEntry { DefaultText = "type me", Placeholder = "search" },

            new Text( "message" ),
            new Goo.TextEntry
            {
                Value       = _value,
                Placeholder = "controlled",
                OnChange    = v => _value = v,
                OnSubmit    = v => _submitted = v,
            },
            new Text( $"echo: \"{_value}\"   submitted: \"{_submitted}\"" ),
        },
    };
}

type in the message field and watch the echo line update. press enter and watch the submitted line update. the label is a pure read of _value, so it is always correct.

add validation to the message field

require the message field to hold something before it counts as valid. Validate runs after every edit and returns whether the current text passes; OnValidationChanged fires once, with the new state, only when validity flips. wire the border to _invalid for a live cue.

add a _invalid field, then extend the message entry:

bool _invalid;

new Goo.TextEntry
{
    Value       = _value,
    Placeholder = "controlled",
    Validate    = v => v.Length > 0,
    OnValidationChanged = invalid => _invalid = invalid,
    BorderWidth = 1,
    BorderColor = _invalid ? Color.Red : Color.Transparent,
    OnChange    = v => _value = v,
    OnSubmit    = v => _submitted = v,
},

Validate runs live as you type, not just on submit. OnValidationChanged is a TextEntry handler like OnChange, so setting _invalid inside it is enough - no separate Rebuild() call, the handler's own auto-rebuild repaints the border. validity is also computed once when the entry is first created, so a fresh field with _value = "" starts out failing v.Length > 0 and the border is red from the first mount, before you type anything; type one character and it clears, delete it back to empty and it turns red again.

add a numeric field

set Numeric to restrict the field to numbers. MinValue and MaxValue clamp the accepted range on submit. NumberFormat controls how the value displays (standard C# format strings).

new Text( "quantity (0-100)" ),
new Goo.TextEntry
{
    DefaultText  = "42.00",
    Numeric      = true,
    MinValue     = 0,
    MaxValue     = 100,
    NumberFormat = "0.00",
    MaxLength    = 8,
},

the finished class with all three fields:

using Goo;
using Sandbox.UI;

public class InputFormUI : GooPanel<Container>
{
    string _value = "";
    string _submitted = "(nothing yet)";
    bool _invalid;

    protected override Container Build() => new Container
    {
        Padding = 16,
        Width = 360,
        FlexDirection = FlexDirection.Column,
        Gap = 8,
        Children =
        {
            new Text( "name" ),
            new Goo.TextEntry { DefaultText = "type me", Placeholder = "search" },

            new Text( "message" ),
            new Goo.TextEntry
            {
                Value       = _value,
                Placeholder = "controlled",
                Validate    = v => v.Length > 0,
                OnValidationChanged = invalid => _invalid = invalid,
                BorderWidth = 1,
                BorderColor = _invalid ? Color.Red : Color.Transparent,
                OnChange    = v => _value = v,
                OnSubmit    = v => _submitted = v,
            },
            new Text( $"echo: \"{_value}\"   submitted: \"{_submitted}\"" ),

            new Text( "quantity (0-100)" ),
            new Goo.TextEntry
            {
                DefaultText  = "42.00",
                Numeric      = true,
                MinValue     = 0,
                MaxValue     = 100,
                NumberFormat = "0.00",
                MaxLength    = 8,
            },
        },
    };
}

what just happened

the three entries show three distinct modes:

the controlled pattern (field + OnChange) is the same state loop from your first counter, with one difference: you never call Rebuild() by hand here. every TextEntry handler runs through the same AutoRebuildOnEvents path described in build method, so mutating the field inside the handler is the whole job.

see also