User Input (Forms)
The Neo.mjs Forms Engine provides a powerful and flexible way to build user interfaces for data input and validation. This guide will walk you through the core concepts and practical usage of forms in Neo.mjs, from basic field creation to advanced validation and nested structures.
1. Basic Form Creation
At its core, a form in Neo.mjs is a Neo.form.Container. This container
manages a collection of form fields and provides methods for data
retrieval, validation, and submission.
To create a simple form, you define a Neo.form.Container and add
Neo.form.field.Text or other field modules to its items config.
import FormContainer from '../../src/form/Container.mjs';
import TextField from '../../src/form/field/Text.mjs';
class MySimpleForm extends FormContainer {
static config = {
className: 'MySimpleForm',
layout : {ntype: 'vbox', align: 'start'},
items : [{
module : TextField,
labelText: 'First Name',
name : 'firstName',
required : true
}, {
module : TextField,
labelText: 'Last Name',
name : 'lastName'
}]
}
}
In this example:
module: TextFieldspecifies the type of form field.labelTextdefines the visible label for the field.nameis crucial for data management; it defines the key under which the field's value will be stored when retrieving form data.required: trueenables basic validation, ensuring the field is not left empty.
2. Field Types
Neo.mjs offers a rich set of pre-built form field types, all extending
Neo.form.field.Base. These fields cover a wide range of input needs:
- Text-based Inputs:
TextField,TextArea,EmailField,PasswordField,PhoneField,UrlField,SearchField,DisplayField(read-only),HiddenField. - Numeric Inputs:
NumberField,CurrencyField,RangeField. - Selection Inputs:
ComboBox,Chip,ColorField,DateField,TimeField,CountryField,ZipCodeField. - Choice Inputs:
CheckBox,Radio,Switch. - File Upload:
FileUpload.
You can find the full list of available fields and their configurations
in the src/form/field/ directory.
3. Data Management
A key strength of Neo.mjs forms is their integrated state management. The Neo.form.Container automatically manages form data based on field names, eliminating the need for external state management libraries or manual state tree definitions. This significantly simplifies data handling and reduces boilerplate.
The Neo.form.Container provides powerful methods for managing form data.
Getting Form Values
To retrieve all values from a form, use the asynchronous getSubmitValues()
method. This method returns a plain JavaScript object where keys correspond
to the name attributes of your fields.
// Assuming 'myForm' is an instance of your form container
const formValues = await myForm.getSubmitValues();
console.log(formValues);
// Example output: { firstName: 'John', lastName: 'Doe' }
Nested Data Structures
The name attribute supports dot notation to create nested data structures.
This is particularly useful for organizing complex form data.
import FormContainer from '../../src/form/Container.mjs';
import TextField from '../../src/form/field/Text.mjs';
class UserForm extends FormContainer {
static config = {
className: 'UserForm',
layout : {ntype: 'vbox', align: 'start'},
items : [{
module : TextField,
labelText: 'User First Name',
name : 'user.profile.firstName'
}, {
module : TextField,
labelText: 'User Last Name',
name : 'user.profile.lastName'
}, {
module : TextField,
labelText: 'Address Street',
name : 'user.address.street'
}]
}
}
// ... later, after the form is rendered and values are entered
const userFormValues = await myUserForm.getSubmitValues();
console.log(userFormValues);
/*
Output:
{
user: {
profile: {
firstName: 'Jane',
lastName : 'Doe'
},
address: {
street: '123 Main St'
}
}
}
*/
Setting Form Values
You can pre-populate a form or update its values programmatically using
the setValues(values, suspendEvents) method. The values object should
mirror the structure of the data returned by getSubmitValues().
// To set values for the UserForm example above:
await myUserForm.setValues({
user: {
profile: {
firstName: 'Alice',
lastName : 'Smith'
},
address: {
street: '456 Oak Ave'
}
}
});
The optional suspendEvents parameter (default false) can be set to
true to prevent change events from firing for each field during the
update, which can be useful for large data sets.
Resetting Forms
The reset(values) method allows you to clear or reset form fields.
If no values object is provided, fields will be reset to null or their
emptyValue (if configured). If values are provided, fields will be
reset to those specific values.
// Reset all fields to their default empty state
await myForm.reset();
// Reset specific fields to new values
await myForm.reset({
firstName: 'Default Name'
});
4. Validation
Neo.mjs forms provide robust validation capabilities, both built-in and customizable.
Built-in Validation
Many field types come with built-in validation rules:
required: Ensures a field is not empty.minLength/maxLength: For text-based fields, validates the length of the input.minValue/maxValue: For numeric fields, validates the range of the input.inputPattern: A regular expression to validate the input format. (e.g.,EmailField,UrlField,ZipCodeFielduse this internally).
You can configure these directly on the field:
import TextField from '../../src/form/field/Text.mjs';
// ...
items: [{
module : TextField,
inputPattern: /^[a-zA-Z0-9_]+$/, // Alphanumeric and underscore only
labelText : 'Username',
name : 'username',
required : true,
minLength : 5,
maxLength : 20
}]
Error messages for built-in validations can be customized using errorText*
configs (e.g., errorTextRequired, errorTextMaxLength).
Custom Validation (validator)
For more complex validation logic, you can use the validator config on
any field. This should be a function that receives the field instance as
its argument and returns true if the value is valid, or a string
(the error message) if it's invalid.
import TextField from '../../src/form/field/Text.mjs';
// ...
items: [{
module : TextField,
labelText: 'Password',
name : 'password',
reference: 'passwordField' // Add a reference to the password field
}, {
module : TextField,
labelText: 'Confirm Password',
name : 'confirmPassword',
validator: function(field) {
// Access the password field using getClosestForm().getReference()
const passwordField = field.getClosestForm().getReference('passwordField');
if (field.value !== passwordField.value) {
return 'Passwords do not match'
}
return true
}
}]
Displaying Errors
Errors are automatically displayed below the field when validation fails.
The useAlertState config (globally set in apps/form/Overwrites.mjs)
can change the visual styling of required but empty fields from red to orange.
The clean property on a field determines if errors are shown immediately.
By default, clean is true until the user interacts with the field or
validate(false) is called.
Form Validation State
You can check the overall validity of a form using:
isValid(): An asynchronous method that returnstrueif all fields in the form (and its nested forms) are valid,falseotherwise. It also triggers validation for all fields.const formIsValid = await myForm.isValid(); if (formIsValid) { console.log('Form is valid!'); } else { console.log('Form has errors.'); }getFormState(): An asynchronous method that returns a string indicating the overall state of the form:'clean': All fields are untouched and valid.'valid': All fields are valid.'invalid': At least one field is invalid.'inProgress': Some fields are valid, some are clean.
const state = await myForm.getFormState(); console.log('Form state:', state);
5. Nested Forms
Neo.mjs allows for true nested forms, providing unparalleled structural
flexibility. This is achieved by nesting Neo.form.Container instances
or using Neo.form.Fieldset.
Using Neo.form.Fieldset
Neo.form.Fieldset extends Neo.form.Container and is ideal for
grouping related fields visually. It can also be collapsed.
import Fieldset from '../../src/form/Fieldset.mjs';
import FormContainer from '../../src/form/Container.mjs';
import TextField from '../../src/form/field/Text.mjs';
class NestedFieldsetForm extends FormContainer {
static config = {
className: 'NestedFieldsetForm',
layout : {ntype: 'vbox', align: 'start'},
items : [{
module : Fieldset,
title : 'Personal Information',
formGroup: 'person', // Data will be nested under 'person'
items : [{
module : TextField,
labelText: 'First Name',
name : 'firstName',
required : true
}, {
module : TextField,
labelText: 'Last Name',
name : 'lastName'
}]
}, {
module : Fieldset,
title : 'Contact Information',
formGroup: 'contact', // Data will be nested under 'contact'
items : [{
module : TextField,
labelText: 'Email',
name : 'email',
required : true
}, {
module : TextField,
labelText: 'Phone',
name : 'phone'
}]
}]
}
}
// Example getSubmitValues() output:
/*
{
person: {
firstName: 'John',
lastName : 'Doe'
},
contact: {
email: 'john.doe@example.com',
phone: '123-456-7890'
}
}
*/
The formGroup config on Fieldset (or any Form.Container) automatically
nests the data of its child fields under the specified key.
Nesting Form.Container Instances
You can directly nest Form.Container instances to create more complex
hierarchies. This is demonstrated in apps/form/view/FormPageContainer.mjs,
which extends Neo.form.Container but uses a div for its vdom tag
to avoid invalid HTML (<form> inside <form>).
// Example from apps/form/view/FormPageContainer.mjs
import BaseFormContainer from '../../../src/form/Container.mjs';
class FormPageContainer extends BaseFormContainer {
static config = {
className: 'Form.view.FormPageContainer',
// ... other configs
tag: 'div' // Using a div instead of a form tag
}
}
This allows you to treat each nested container as a sub-form, which can be validated or have its values retrieved independently, or as part of the top-level form.
6. Field Triggers
Field triggers are small, interactive icons or buttons that appear within or alongside a form field, providing additional functionality. Examples include clear buttons, date pickers, or spin buttons for number fields.
Triggers are configured via the triggers array on a field. You can
configure multiple triggers for a single field, and control their placement
(left or right of the input) using the align config on the trigger.
import DateField from '../../src/form/field/Date.mjs';
import ClearTrigger from '../../src/form/field/trigger/Clear.mjs';
// ...
items: [{
module : DateField,
labelText: 'Event Date',
name : 'eventDate',
triggers : [{
module: ClearTrigger // Adds a clear button to the date field
}]
}]
Many fields automatically include default triggers (e.g., DateField
includes a DateTrigger). You can override or add to these defaults.
7. Form and Field Events
Neo.mjs forms and fields emit various events that you can listen to for custom logic:
change: Fired when a field'svalueconfig changes.userChange: Fired when a field's value changes due to direct user interaction (e.g., typing in a text field).fieldChange: Fired on theForm.Containerwhen any of its child fields'valuechanges.fieldUserChange: Fired on theForm.Containerwhen any of its child fields' value changes due to user interaction.focusEnter/focusLeave: Fired when a field gains or loses focus.
You can listen to these events using the on method:
manyFormField.on('change', (data) => {
console.log('Field value changed:', data.value);
});
myFormContainer.on('fieldUserChange', (data) => {
console.log('User changed field:', data.component.name, data.value);
});
8. Best Practices and Tips
itemDefaults: UseitemDefaultson containers to apply common configurations to all child items, reducing boilerplate.formGroup: LeverageformGroupfor logical grouping of data, especially in complex forms, to create clean nested data structures.readOnlyvs.editable:readOnly: true: Prevents user interaction from changing the value. The field is still part of the form data.editable: false: Similar toreadOnly, but specifically for input elements, preventing direct typing. Other interactions (like picker selection) might still be possible unless alsoreadOnly.
- Lazy Loading: For forms with many pages or complex sections, consider
lazy loading modules for individual pages or fieldsets to improve initial
application load times. This is demonstrated in
apps/form/view/FormContainer.mjswhere pages are imported dynamically. - Explicit Module Imports: While the core
Neoglobal namespace is always available, it's a best practice to explicitly import all Neo.mjs modules you use (e.g.,import FormContainer from '../../src/form/Container.mjs';). Relying on implicit availability of classes withinNeo's sub-namespaces can lead to less readable and maintainable code. Explicit imports improve code readability, maintainability, and ensure consistent behavior. Neo.overwrites: Use global overwrites (as seen inapps/form/Overwrites.mjs) to enforce consistent styling or behavior across all instances of a component type.