jaspr_forms 0.1.1
jaspr_forms: ^0.1.1 copied to clipboard
Form validation and management for Jaspr. Declarative validators, field state tracking, and form submission with support for all 3 rendering modes (static/server/client).
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
import 'package:jaspr_forms/jaspr_forms.dart';
void main() {
runApp(App());
}
class App extends StatelessComponent {
const App({super.key});
@override
Component build(BuildContext context) {
return div(classes: 'container', [
h1([.text('Jaspr Forms Example')]),
p([.text('A registration form demonstrating validation with jaspr_forms.')]),
const RegistrationForm(),
]);
}
}
@client
class RegistrationForm extends StatefulComponent {
const RegistrationForm();
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
String? _submitted;
@override
Component build(BuildContext context) {
return FormBuilder(
validators: {
'username': [
Required(message: 'Username is required').call,
MinLength(3, message: 'Username must be at least {min} characters').call,
Custom((v) => v.contains(' ') ? 'Username cannot contain spaces' : null).call,
],
'email': [Required(message: 'Email is required').call, Email(message: 'Enter a valid email address').call],
'password': [
Required(message: 'Password is required').call,
MinLength(8, message: 'Password must be at least {min} characters').call,
Custom((v) {
if (!RegExp(r'[A-Z]').hasMatch(v)) return 'Password must contain an uppercase letter';
if (!RegExp(r'[0-9]').hasMatch(v)) return 'Password must contain a number';
return null;
}).call,
],
'age': [
Required(message: 'Age is required').call,
Custom((v) {
final n = int.tryParse(v);
if (n == null) return 'Age must be a number';
if (n < 18) return 'You must be at least 18 years old';
if (n > 120) return 'Please enter a valid age';
return null;
}).call,
],
'bio': [MaxLength(200, message: 'Bio must be at most {max} characters').call],
},
initialValues: {'bio': ''},
onSubmit: (values) async {
await Future.delayed(const Duration(milliseconds: 800));
_submitted = values.toString();
},
onChanged: (_) {},
children: [
_Field(name: 'username', label: 'Username', inputType: InputType.text),
_Field(name: 'email', label: 'Email', inputType: InputType.email),
_Field(name: 'password', label: 'Password', inputType: InputType.password),
_Field(name: 'age', label: 'Age', inputType: InputType.number),
_Field(name: 'bio', label: 'Bio', isTextarea: true),
div(classes: 'btn-row', [
button(classes: 'primary', disabled: context.form.isSubmitting, onClick: () => context.form.submit(), [
.text(context.form.isSubmitting ? 'Submitting...' : 'Register'),
]),
button(
classes: 'secondary',
onClick: () {
context.form.reset();
_submitted = null;
setState(() {});
},
[.text('Reset')],
),
]),
if (_submitted != null)
div(classes: 'success-card', [
h3([.text('Registration Successful')]),
pre([.text(_submitted!)]),
]),
],
);
}
}
class _Field extends StatefulComponent {
final String name;
final String label;
final InputType? inputType;
final bool isTextarea;
const _Field({required this.name, required this.label, this.inputType, this.isTextarea = false});
@override
State<_Field> createState() => _FieldState();
}
class _FieldState extends State<_Field> {
String _value = '';
@override
Component build(BuildContext context) {
final error = context.form.field(component.name)?.error;
final attrs = <String, String>{if (component.inputType != null) 'type': component.inputType!.name, 'placeholder': component.label};
return div(classes: 'form-group', [
label([.text(component.label)]),
if (component.isTextarea)
textarea(
[],
classes: error != null ? 'error' : null,
attributes: attrs,
events: {
'input': (e) {
final v = (e.target as dynamic).value as String? ?? '';
_value = v;
context.form.updateField(component.name, v);
},
},
)
else
input(
type: component.inputType ?? InputType.text,
classes: error != null ? 'error' : null,
value: _value,
onInput: (v) {
final str = v.toString();
_value = str;
context.form.updateField(component.name, str);
},
),
if (error != null) div(classes: 'error-text', [.text(error)]),
]);
}
}