Get the FREE Ultimate OpenClaw Setup Guide →

nylo-forms

Scanned
npx machina-cli add skill nylo-core/claude-code/nylo-forms --openclaw
Files (1)
SKILL.md
14.2 KB

Nylo Forms

Overview

Nylo v7 forms are built around NyFormWidget, which IS the widget itself (no separate wrapper needed). Forms define fields via a fields() method, support 22 field types, provide built-in validation through FormValidator, and return data as snake_case Map<String, dynamic> on submission.

When to Use

  • Building any form (login, registration, edit profile, settings)
  • Adding validated input fields to a page
  • Creating picker, radio, checkbox, slider, or date fields
  • Validating user input with built-in or custom rules
  • Submitting form data to an API
  • Laying out fields side-by-side in rows
  • When NOT to use: For standalone text input without form context, use InputField directly

Quick Reference

ActionCode
Create form (CLI)metro make:form LoginForm
Text fieldField.text("Name")
Email fieldField.email("Email")
Password fieldField.password("Password", viewable: true)
Number fieldField.number("Age")
Picker fieldField.picker("Country", options: FormCollection.from([...]))
Validate not emptyvalidator: FormValidator.notEmpty()
Validate emailvalidator: FormValidator.email()
Validate passwordvalidator: FormValidator.password(strength: 2)
Chain validatorsFormValidator().notEmpty().minLength(3).maxLength(20)
Custom validatorFormValidator.custom(validate: (data) => ..., message: "...")
Submit via actionsMyForm.actions.submit(onSuccess: (data) { })
Update fieldMyForm.actions.updateField("Name", "Jane")
Clear all fieldsMyForm.actions.clear()

Form Structure

Generate a form with CLI:

metro make:form LoginForm

Every form extends NyFormWidget:

class LoginForm extends NyFormWidget {
  LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  fields() => [
    Field.email("Email", validator: FormValidator.email()),
    Field.password("Password", validator: FormValidator.password()),
  ];

  static NyFormActions get actions => const NyFormActions('LoginForm');
}

Field Types

Text-Based Fields

Field.text("Name")                                    // Standard text
Field.email("Email")                                  // Email keyboard + filtering
Field.password("Password", viewable: true)            // Obscured + toggle
Field.number("Age", decimal: true)                    // Numeric keyboard
Field.url("Website")                                  // URL keyboard
Field.textArea("Description")                         // Multi-line
Field.phoneNumber("Mobile")                           // Auto-formatted phone
Field.currency("Price", currency: "usd")              // Currency formatted
Field.mask("Phone", mask: "(###) ###-####")           // Pattern mask
Field.capitalizeWords("Full Name")                    // Title case
Field.capitalizeSentences("Bio")                      // Sentence case

Selection Fields

// Picker (single selection from list)
Field.picker("Category",
  options: FormCollection.from(["Tech", "Science", "Art"]),
)

// Radio buttons
Field.radio("Newsletter",
  options: FormCollection.fromMap({"yes": "Yes", "no": "No"}),
)

// Checkbox (boolean toggle)
Field.checkbox("Accept Terms")

// Switch (boolean toggle)
Field.switchBox("Enable Notifications")

// Chips (multi-select)
Field.chips("Tags",
  options: FormCollection.from(["Flutter", "Dart", "Nylo"]),
)

Range Fields

// Slider (single value)
Field.slider("Rating",
  style: FieldStyleSlider(min: 0, max: 10),
)

// Range slider (two values)
Field.rangeSlider("Price Range",
  style: FieldStyleRangeSlider(min: 0, max: 1000),
)

Date/Time Fields

Field.date("Birthday")
Field.datetime("Appointment", firstDate: DateTime(2025))

Special Fields

// Custom field (child must extend NyFieldStatefulWidget)
Field.custom("My Field", child: MyCustomFieldWidget())

// Embed any widget (no field functionality)
Field.widget(child: Divider())

FormCollection

Required for picker, radio, and chips fields:

// From list (value and label are the same)
FormCollection.from(["Red", "Green", "Blue"])

// From map (key = value, value = label)
FormCollection.fromMap({"us": "United States", "ca": "Canada"})

// From structured data
FormCollection.fromKeyValue([
  {"value": "en", "label": "English"},
  {"value": "es", "label": "Spanish"},
])

Query methods: getByValue(), getLabelByValue(), containsValue(), searchByLabel(), values, labels.

Validation

Built-In Validators

FormValidator.email()                        // Email format
FormValidator.password(strength: 2)          // Strength 1: 8+ chars, upper, digit
                                             // Strength 2: adds special char
FormValidator.notEmpty()                     // Rejects empty
FormValidator.minLength(5)                   // Min string length
FormValidator.maxLength(100)                 // Max string length
FormValidator.minValue(18)                   // Min numeric value
FormValidator.maxValue(100)                  // Max numeric value
FormValidator.url()                          // URL format
FormValidator.numeric()                      // Numeric check
FormValidator.booleanTrue()                  // Must be true
FormValidator.phoneNumberUs()                // US phone format
FormValidator.phoneNumberUk()               // UK phone format
FormValidator.zipcodeUs()                    // US zipcode
FormValidator.date()                         // Valid date
FormValidator.dateInPast()                   // Date in past
FormValidator.dateInFuture()                 // Date in future
FormValidator.dateAgeIsOlder(18)             // Age >= 18
FormValidator.uppercase()                    // All uppercase
FormValidator.lowercase()                    // All lowercase
FormValidator.regex(r'^[A-Z]{3}\d{4}$')     // Regex pattern

Chaining Validators

Field.text("Username",
  validator: FormValidator()
      .notEmpty(message: "Username is required")
      .minLength(3, message: "At least 3 characters")
      .maxLength(20, message: "At most 20 characters"),
)

Custom Validator

Field.number("Age",
  validator: FormValidator.custom(
    message: "Age must be between 18 and 100",
    validate: (data) {
      int? age = int.tryParse(data.toString());
      return age != null && age >= 18 && age <= 100;
    },
  ),
)

Reusable Custom Rule

class FormRuleUsername extends FormRule {
  @override
  String? rule = "username";

  @override
  String? message = "The {{attribute}} must be a valid username.";

  FormRuleUsername({String? message}) {
    if (message != null) this.message = message;
  }

  @override
  bool validate(data) {
    if (data is! String) return false;
    return RegExp(r'^[a-zA-Z0-9_]{3,20}$').hasMatch(data);
  }
}

// Apply with FormValidator.rule()
FormValidator validator = FormValidator.rule([
  FormRuleNotEmpty(),
  FormRuleUsername(),
]);

Page-Level Validation with check()

Validate outside of forms using check() in NyPage:

check((validate) {
  validate.that(_emailController.text, label: "Email").email();
  validate.that(_passwordController.text, label: "Password")
      .notEmpty()
      .password(strength: 2);
}, onSuccess: () {
  _submitForm();
}, onValidationError: (FormValidationResponseBag bag) {
  print(bag.firstErrorMessage);
});

Displaying Forms

The form IS the widget, use it directly in your build method:

@override
Widget view(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: LoginForm(
        submitButton: Button.primary(text: "Login"),
        onSubmit: (data) {
          // data = {"email": "...", "password": "..."}
          print(data);
        },
        onFailure: (errors) {
          print(errors.first.rule.getMessage());
        },
      ),
    ),
  );
}

Form Constructor Parameters

ParameterTypePurpose
submitButtonWidget?Submit button widget
onSubmitFunction(Map)?Success callback with form data
onFailureFunction(List)?Validation failure callback
initialDataMap<String, dynamic>?Pre-populate fields
onChangedFunction(Field, dynamic)?Field change callback
crossAxisSpacingdoubleHorizontal spacing between row fields
mainAxisSpacingdoubleVertical spacing between fields
headerWidget?Widget above form
footerWidget?Widget below form
lockedboolMake entire form read-only
loadingStyleLoadingStyle?Loading indicator style

Submission

Method 1: onSubmit Callback

LoginForm(
  submitButton: Button.primary(text: "Login"),
  onSubmit: (data) {
    // {email: "user@email.com", password: "pass123"}
  },
)

Method 2: NyFormActions

LoginForm.actions.submit(
  onSuccess: (data) { print(data); },
  onFailure: (errors) { print(errors.first.rule.getMessage()); },
  showToastError: true,
);

Method 3: Static Submit

NyFormWidget.submit("LoginForm",
  onSuccess: (data) { print(data); },
);

Initial Data and Dynamic Fields

Using init Getter

class EditAccountForm extends NyFormWidget {
  @override
  Function()? get init => () async {
    final user = await api<ApiService>((r) => r.getUserData());
    final countries = await api<ApiService>((r) => r.getCountries());
    return {
      "First Name": user?.firstName,
      "Last Name": user?.lastName,
      "Country": define(value: user?.country, options: countries),
    };
  };
  // ...
}

Use define() for fields that need both a value and dynamic options (pickers, chips, radios).

Using initialData Parameter

EditAccountForm(
  initialData: {
    "first_name": "John",
    "last_name": "Doe",
  },
)

Form Actions (NyFormActions)

static NyFormActions get actions => const NyFormActions('MyForm');
MethodPurpose
updateField(key, value)Set a field's value
clearField(key)Clear a specific field
clear()Clear all fields
refresh()Refresh form UI state
refreshForm()Re-call fields() and rebuild entirely
setOptions(key, options)Update picker/chip/radio options
submit(onSuccess:, onFailure:)Submit with validation

Form Layout

Place fields in a list to render them side-by-side in a row:

@override
fields() => [
  Field.text("Title"),

  // Two fields in one row
  [
    Field.text("First Name"),
    Field.text("Last Name"),
  ],

  Field.textArea("Bio"),
  Field.widget(child: Divider()),
  Field.email("Email"),
];

Field Styling

Each field type has a corresponding style class:

Field.text("Name",
  style: FieldStyleTextField(
    filled: true,
    fillColor: Colors.grey.shade100,
    border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
    contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    prefixIcon: Icon(Icons.person),
  ),
)

Picker Styles

Field.picker("Country",
  options: FormCollection.from(["US", "CA", "UK"]),
  style: FieldStylePicker(
    listTileStyle: PickerListTileStyle.radio(activeColor: Colors.blue),
    // Or: PickerListTileStyle.checkmark(activeColor: Colors.green)
    // Or: PickerListTileStyle.custom(builder: (option, isSelected, onTap) => ...)
  ),
)

Complete Example

class RegisterForm extends NyFormWidget {
  RegisterForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  fields() => [
    [
      Field.text("First Name", validator: FormValidator.notEmpty()),
      Field.text("Last Name", validator: FormValidator.notEmpty()),
    ],
    Field.email("Email", validator: FormValidator.email()),
    Field.password("Password",
      viewable: true,
      validator: FormValidator.password(strength: 2),
    ),
    Field.phoneNumber("Phone",
      validator: FormValidator.phoneNumberUs(),
    ),
    Field.picker("Country",
      options: FormCollection.from(["US", "CA", "UK", "AU"]),
      validator: FormValidator.notEmpty(),
    ),
    Field.checkbox("Accept Terms",
      validator: FormValidator.booleanTrue(
        message: "You must accept the terms"),
    ),
  ];

  static NyFormActions get actions => const NyFormActions('RegisterForm');
}

// Usage in page
@override
Widget view(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: RegisterForm(
        submitButton: Button.primary(text: "Register"),
        onSubmit: (data) async {
          // data = {first_name, last_name, email, password, phone, country, accept_terms}
          await api<AuthApiService>((r) => r.register(data));
          routeTo(HomePage.path);
        },
        onFailure: (errors) {
          showToastDanger(description: errors.first.rule.getMessage());
        },
      ),
    ),
  );
}

Common Mistakes

MistakeFix
Using raw lists for picker/radio/chips optionsMust use FormCollection.from(), .fromMap(), or .fromKeyValue()
Field key mismatch with initialDataField keys are title-cased ("First Name"), but initialData uses snake_case ("first_name"); both formats work for initialData
Not defining static NyFormActions get actionsRequired for using MyForm.actions.submit() and other programmatic interactions
Forgetting define() for dynamic picker options in initWhen setting both value and options in init, wrap with define(value: ..., options: ...)
Expecting non-null from onSubmit dataForm data values can be null if fields are left empty; always handle nulls
Using FormValidator.password() without specifying strengthDefaults to strength 1 (8+ chars, 1 upper, 1 digit); use strength: 2 for special char requirement
Placing Field.widget() and expecting form dataField.widget() is for embedding non-field widgets like dividers; it does not participate in form data
Not calling super.onTap(index) equivalent for custom submitWhen using a custom footer button, call MyForm.actions.submit() explicitly

Source

git clone https://github.com/nylo-core/claude-code/blob/main/skills/nylo-forms/SKILL.mdView on GitHub

Overview

Nylo v7 forms are built around NyFormWidget, the form itself. They define inputs via a fields() method, support 22 field types, use FormValidator for validation, and return submitted data as a snake_case Map<String, dynamic>.

How This Skill Works

Forms extend NyFormWidget and declare inputs in fields(). Each field is created with Field.* builders and optional validators from FormValidator. On submission, NyFormActions handles the process and returns the input data as a snake_case Map<String, dynamic> for API calls or further processing.

When to Use It

  • Building login, registration, edit profile, or settings forms
  • Adding validated input fields to an existing page
  • Creating picker, radio, checkbox, slider, or date fields
  • Validating user input with built-in or custom rules
  • Submitting form data to an API

Quick Start

  1. Step 1: Generate form via CLI: metro make:form LoginForm
  2. Step 2: Extend NyFormWidget and define fields() with Field.* and FormValidator rules
  3. Step 3: Submit using MyForm.actions.submit(onSuccess: (data) { ... }) and handle responses

Best Practices

  • Define all inputs in a clear fields() method to keep forms maintainable
  • Leverage FormValidator.notEmpty(), email(), password(), etc., for common rules
  • Use FormCollection for consistent picker/radio/checkbox options
  • Utilize NyFormWidget layout capabilities to arrange fields in rows
  • Test onSubmit flows and handle onSuccess/onFailure with NyFormActions

Example Use Cases

  • LoginForm with Field.email and Field.password using FormValidator.email() and FormValidator.password()
  • RegistrationForm collecting name, email, password, and terms acceptance with appropriate validators
  • ProfileSettingsForm including text fields, a date field, and a toggle/checkbox for preferences
  • CountryPickerForm using Field.picker with FormCollection.from([...]) and notEmpty validation
  • ProductFilterForm featuring Field.slider and Field.rangeSlider for price and rating filters

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers