Validate what players type

Check text and number input with validators, show errors under the field, and enable a Save button only while every field is valid.

Last updated

Steps

Four widgets take typed input: TextInput, NumberField, MultiLineInput and the input of a StringListEditor. They all validate the same way.

How validation works#

Validate a text field#

TextInput name = TextInput.builder("name")
        .label(Text.translatable("mymod.name", "Profile name"))
        .initial(config.profileName)
        .maxLength(32)
        .validator(s -> s.trim().isEmpty()
                ? Text.translatable("mymod.name.empty", "Enter a name")
                : null)
        .onChange(s -> config.profileName = s)
        .build();

maxLength is a hard limit: the field accepts no more characters, so you don't need to validate length.

Validate a number#

NumberField checks the number itself. It shows these errors with no validator at all:

Input Error
not a number, or empty "Not a number"
a decimal in an ofInt field (2.5; 2.0 is accepted as 2) "Not a whole number"
below range's minimum "Must be at least %s"
above range's maximum "Must be at most %s"

Your own validator runs last, only on numbers that passed those checks:

NumberField<Integer> port = NumberField.ofInt("port")
        .label(Text.translatable("mymod.port", "Port"))
        .initial(config.port)
        .range(1024, 65535)
        .validator(n -> n == 8080 ? Text.translatable("mymod.port.taken", "8080 is reserved") : null)
        .onChange(n -> config.port = n)
        .build();

Numbers are parsed with . as the decimal separator in every language, and 1e3 is accepted.

Enable Save only while the input is valid#

Keep a reference to the field and read isValid() from the button's enabled supplier. A disabled button shows its tooltip too, which is a good place to say why:

Button create = Button.builder("create")
        .label(Text.translatable("mymod.create", "Create"))
        .enabled(name::isValid)
        .tooltip(() -> name.isValid() ? null : Text.translatable("mymod.create.fix", "Fix the name first"))
        .onPress(() -> {
            store.create(name.value());
            host.back();
        })
        .build();

With several fields, combine them: .enabled(() -> name.isValid() && port.input().isValid()). NumberField.input() is the text field it is drawn as.

Read the input in an action#

Instead of mirroring every edit with onChange, an action can read the field when it runs:

Widget Current input Valid?
TextInput value(), valid or not isValid(), error()
MultiLineInput value(), with \n line breaks isValid(), error()
NumberField number() input().isValid()

To replace the text from code, for example to clear a field after using it, call setValue("") on a TextInput or MultiLineInput.

Validate list items#

A StringListEditor already refuses empty input and items that are in the list ("Already in the list"). Add your own rule with validator:

StringListEditor.builder("servers")
        .items(() -> config.servers)
        .onAdd(config.servers::add)
        .onRemove(config.servers::remove)
        .inputLabel(Text.translatable("mymod.servers.add", "Server address"))
        .validator(s -> s.contains(" ") ? Text.translatable("mymod.servers.space", "No spaces") : null)
        .build();

Test it#

FakeHost.type(id, text) types into a field; render() shows the error:

host.type("port", "80");
assertTrue(host.render().contains("(error: Must be at least 1024)"));

Next steps#