Port
When building forms in Flutter, developers often encounter challenges such as manual form definition, validation logic, and UI consistency. The Port Module addresses these pain points by providing a powerful API for defining form structures, handling validation, and automatically generating user interface components.
Core Concepts
Port
A Port
represents the definition of a form. It serves three primary purposes:
- Defining the fields of the form.
- Validating the form data.
- Transforming and returning the form values.
Here's an example of creating a Port
:
final port = Port.of({
'name': PortField.string().isNotBlank(),
'email': PortField.string().isNotBlank().isEmail(),
});
In this example, the Port
defines two fields: "name" and "email". The "name" field is a non-blank string, while the "email" field is a non-blank string that must be in a valid email format.
Fields
The Port Module provides a set of default PortField
types to cover common form field scenarios:
PortField.string()
: Represents a non-nullable String field.PortField.int()
: Represents an int field.PortField.bool()
: Represents a boolean field.PortField.dateTime()
: Represents a DateTime field.PortField.option(options)
: Allows selecting a value from a predefined list of options.PortField.embedded(port)
: Embeds anotherPort
as a field, enabling nested form structures.PortField.stage(options, portMapper)
: Provides a list of options and maps each option to a specificPort
, allowing dynamic form stages based on user selections.PortField.file()
: Represents a CrossFile.
You can further customize PortField
s by applying modifiers to add validation rules or transform the field data. Some common modifiers include:
.isNotNull()
: Ensures the field value is not null..isNotBlank()
: Ensures a String field is not null or blank..isEmail()
: Validates that a String field is in a valid email format..isPhone()
: Validates that a String field is in a valid phone number format..withAllowedFileTypes()
: Validates that a cross file field has a valid file type..withFallback(fallback)
: Specifies a fallback value to use if the field value is null..map(mapper)
: Transforms the field value using a provided mapper function..withValidator(validator)
: Adds a custom validation rule using aValidator
function.
Submitting and Validating
Once you have defined your Port
and its fields, you can submit the form and validate the results. Here's an example of how to submit a Port
and handle the validation results:
final port = Port.of({
'name': PortField.string().isNotBlank(),
'email': PortField.string().isNotBlank().isEmail(),
});
port.setValue(name: 'name', value: 'John Doe');
port.setValue(name: 'email', value: 'john@test.com');
final result = await port.submit();
result.isValid // true
result.data['name'] // 'John Doe'
Transforming Data
By default, a Port
returns a Map<String, dynamic>
containing the field names as keys and their corresponding values. However, you can transform the form data into a custom object using the .map(mapper)
modifier. This allows you to create a Port
that returns a specific type, making it more convenient to work with the form results.
final userPort = Port.of({
'name': PortField.string().isNotBlank(),
'email': PortField.string().isNotBlank().isEmail(),
}).map((values, port) => User(name: values['name'], email: values['email']));
port.setValue(name: 'name', value: 'John Doe');
port.setValue(name: 'email', value: 'john@test.com');
final userResult = await port.submit();
result.isValid // true
result.data // The mapped `User` object
Flutter Integration
The Port Module seamlessly integrates with Flutter, providing widgets and builders to simplify the rendering and management of form fields within your application.
PortBuilder
The PortBuilder
widget listens to the values of a Port
and rebuilds its child widgets whenever the form data changes. It provides access to the current state of the Port
, allowing you to build custom widgets based on the form values.
PortBuilder(
port: port,
builder: (Port port) {
// Build widgets based on the Port values
}
)
PortFieldBuilder
The PortFieldBuilder
widget focuses on a specific PortField
within a Port
. It listens to the value of the field and rebuilds its child widget whenever the field value changes. This is particularly useful for building custom field widgets or displaying field-specific error messages.
PortFieldBuilder(
fieldName: 'name',
builder: (context, portField, value, error) {
return StyledText.body(value ?? 'Error: $error');
}
)
Auto-generated UI
The Port Module takes form development to the next level by providing the ability to automatically generate user interface components based on a Port
definition. This feature significantly reduces the amount of boilerplate code required to build form screens and ensures a consistent look and feel across your application.
The auto-generated UI considers the different PortField
types, their modifiers, and any additional metadata provided. It intelligently maps each field to an appropriate UI component based on its type and characteristics, and also renders any validation errors underneath each field. For example:
PortField.string()
is rendered as a text input field.PortField.int()
is rendered as a text input field with int validation.PortField.double()
is rendered as a text input field with double validation.PortField.bool()
is rendered as a checkbox.PortField.dateTime()
is rendered as a date picker.PortField.option()
is rendered as a dropdown.PortField.crossFile()
is rendered as a file input field.PortField.stage(options, portMapper)
is rendered as a multi-step form, where the user can select an option and dynamically load and view the correspondingPort
UI.
The auto-generated UI also takes into account the modifiers applied to the fields. For instance:
.withDisplayName()
adds a label above the field..withHint()
adds a hint in the field..isNotNull()
adds a required field indicator..multiline()
sets a text field as multiline..isName()
sets the TextInputType toTextInputType.name
..isEmail()
sets the TextInputType toTextInputType.email
..isPhone()
sets the TextInputType toTextInputType.phoneNumber
and adds an input formatter for phone numbers..isSecret()
masks the input as a password field..multiline()
renders a multiline text input field..withAllowedFileTypes()
sets the file picker to choose only files with the given file extensions..onlyImages()
sets the file picker to choose only images..color()
renders a color picker instead of a regular text input..withSuggestions(suggestionsGetter)
: Given the current value of the PortField, provides an asynchronous list of suggestions to display underneath the field.
The auto-generated UI aims to provide a functional and visually appealing form layout out of the box, saving developers time and effort in building form screens manually. However, it also offers flexibility through overrides and customization options, allowing developers to fine-tune the generated UI to match their specific design requirements.
Rendering a Port
You can render a Port
in various ways:
- As a StyledDialog: Use
context.showDialog(StyledPortDialog(port: port))
to display the form fields in a dialog window. You can override theonAccept
callback to customize the behavior when the user submits the form. - As a widget: Use
StyledObjectPortBuilder(port: port)
to render the form fields as a widget within your application. Useonly
to render only certain fields, andorder
to customize the order the fields will be rendered in.
Here's an example of how to automatically render a port:
final port = useMemoized(() => Port.of({
'name': PortField.string().withDisplayName('Name').withHint('John Doe').isNotBlank().isName(),
'email': PortField.string().withDisplayName('Email').isNotBlank().isEmail(),
'password': PortField.string().withDisplayName('Password').isNotBlank().isSecret(),
'bio': PortField.string().withDisplayName('Bio').multiline(),
'age': PortField.int().withDisplayName('Age').isNotNull(),
'favoriteColor': PortField.int().withDisplayName('Favorite Color').color(),
'profilePicture': PortField.file().withDisplayName('Profile Picture').onlyImages(),
}));
return StyledPage(
body: StyledList.column.withScrollbar(
children: [
StyledObjectPortBuilder(
port: port,
),
],
),
);
}
Overrides
If you need to customize the appearance or behavior of specific form fields, you can provide overrides and define the order of the generated widgets. This allows you to fine-tune the auto-generated UI to match your application's design and requirements.
StyledObjectPortBuilder(
port: userPort, // A port that contains a 'name' and 'email' field.
order: [ // Change the order of the rendered fields.
'email',
'name',
],
overrides: {
'email': StyledTextPortField(
fieldName: 'email',
leadingIcon: Icons.email, // Override the text field with a leading icon email.
),
}
),