Form class
This commit is contained in:
parent
8f025080ac
commit
b50102d611
5 changed files with 153 additions and 10 deletions
|
@ -1 +1,62 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
|
import 'package:angel_validate/angel_validate.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:pretty_logging/pretty_logging.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
Logger.root
|
||||||
|
..level = Level.ALL
|
||||||
|
..onRecord.listen(prettyLog);
|
||||||
|
|
||||||
|
var app = Angel(logger: Logger('angel_validate'));
|
||||||
|
var http = AngelHttp(app);
|
||||||
|
|
||||||
|
var todoForm = Form(fields: [
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
app.get('/', (req, res) {
|
||||||
|
res
|
||||||
|
..contentType = MediaType('text', 'html')
|
||||||
|
..write('''
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form method="POST">
|
||||||
|
<label for="text">Text:</label>
|
||||||
|
<input id="text" name="text">
|
||||||
|
<br>
|
||||||
|
<label for="is_complete">Complete?</label>
|
||||||
|
<input id="is_complete" name="is_complete" type="checkbox">
|
||||||
|
<br>
|
||||||
|
<input type="submit" value="submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.fallback((req, res) => throw AngelHttpException.notFound());
|
||||||
|
|
||||||
|
app.errorHandler = (e, req, res) {
|
||||||
|
res.writeln('Error ${e.statusCode}: ${e.message}');
|
||||||
|
for (var error in e.errors) {
|
||||||
|
res.writeln('* $error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await http.startServer('127.0.0.1', 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Todo {
|
||||||
|
final String text;
|
||||||
|
final bool isComplete;
|
||||||
|
|
||||||
|
Todo(this.text, this.isComplete);
|
||||||
|
|
||||||
|
static Todo fromMap(Map map) {
|
||||||
|
return Todo(map['text'] as String, map['is_complete'] as bool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
export 'src/field.dart';
|
|
||||||
export 'src/form.dart';
|
|
4
lib/angel_validate.dart
Normal file
4
lib/angel_validate.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export 'src/field.dart';
|
||||||
|
export 'src/form.dart';
|
||||||
|
export 'src/form_renderer.dart';
|
||||||
|
export 'src/matchers.dart';
|
|
@ -9,25 +9,24 @@ import 'field.dart';
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// import 'package:angel_forms/angel_forms.dart';
|
|
||||||
/// import 'package:angel_validate/angel_validate.dart';
|
/// import 'package:angel_validate/angel_validate.dart';
|
||||||
///
|
///
|
||||||
/// var myForm = Form(fields: [
|
/// var myForm = Form(fields: [
|
||||||
/// TextField('username').match([]),
|
/// TextField('username').match([minLength(8)]),
|
||||||
/// TextField('password', confirmedAs: 'confirm_password'),
|
/// TextField('password', confirmedAs: 'confirm_password'),
|
||||||
/// ])
|
/// ])
|
||||||
///
|
///
|
||||||
/// app.post('/login', (req, res) async {
|
/// app.post('/login', (req, res) async {
|
||||||
/// var loginRequest =
|
/// var loginBody =
|
||||||
/// await myForm.decode(req, loginRequestSerializer);
|
/// await myForm.decode(req, loginBodySerializer);
|
||||||
/// // Do something with the decode object...
|
/// // Do something with the decoded object...
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
class Form {
|
class Form {
|
||||||
|
/// A custom error message to provide the user if validation fails.
|
||||||
final String errorMessage;
|
final String errorMessage;
|
||||||
|
|
||||||
final List<Field> _fields = [];
|
final List<Field> _fields = [];
|
||||||
final List<String> _errors = [];
|
|
||||||
|
|
||||||
static const String defaultErrorMessage =
|
static const String defaultErrorMessage =
|
||||||
'There were errors in your submission. '
|
'There were errors in your submission. '
|
||||||
|
@ -37,10 +36,11 @@ class Form {
|
||||||
fields?.forEach(addField);
|
fields?.forEach(addField);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the fields in this form.
|
||||||
List<Field> get fields => _fields;
|
List<Field> get fields => _fields;
|
||||||
|
|
||||||
List<String> get errors => _errors;
|
/// Helper for adding fields. Passing [matchers] will result in them
|
||||||
|
/// being applied to the [field].
|
||||||
Field<T> addField<T>(Field<T> field, {Iterable<Matcher> matchers}) {
|
Field<T> addField<T>(Field<T> field, {Iterable<Matcher> matchers}) {
|
||||||
if (matchers != null) {
|
if (matchers != null) {
|
||||||
field = field.match(matchers);
|
field = field.match(matchers);
|
||||||
|
@ -49,15 +49,19 @@ class Form {
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserializes the result of calling [validate].
|
||||||
Future<T> deserialize<T>(
|
Future<T> deserialize<T>(
|
||||||
RequestContext req, T Function(Map<String, dynamic>) f) {
|
RequestContext req, T Function(Map<String, dynamic>) f) {
|
||||||
return validate(req).then(f);
|
return validate(req).then(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Uses the [codec] to [deserialize] the result of calling [validate].
|
||||||
Future<T> decode<T>(RequestContext req, Codec<T, Map> codec) {
|
Future<T> decode<T>(RequestContext req, Codec<T, Map> codec) {
|
||||||
return deserialize(req, codec.decode);
|
return deserialize(req, codec.decode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calls [read], and returns the filtered request body.
|
||||||
|
/// If there is even one error, then an [AngelHttpException] is thrown.
|
||||||
Future<Map<String, dynamic>> validate(RequestContext req) async {
|
Future<Map<String, dynamic>> validate(RequestContext req) async {
|
||||||
var result = await read(req);
|
var result = await read(req);
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
|
|
76
lib/src/matchers.dart
Normal file
76
lib/src/matchers.dart
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:matcher/matcher.dart';
|
||||||
|
|
||||||
|
final RegExp _alphaDash = RegExp(r'^[A-Za-z0-9_-]+$');
|
||||||
|
final RegExp _alphaNum = RegExp(r'^[A-Za-z0-9]+$');
|
||||||
|
final RegExp _email = RegExp(
|
||||||
|
r"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$");
|
||||||
|
final RegExp _url = RegExp(
|
||||||
|
r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)');
|
||||||
|
|
||||||
|
/// Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores.
|
||||||
|
final Matcher isAlphaDash = predicate(
|
||||||
|
(value) => value is String && _alphaDash.hasMatch(value),
|
||||||
|
'alphanumeric (dashes and underscores are allowed)');
|
||||||
|
|
||||||
|
/// Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores.
|
||||||
|
///
|
||||||
|
final Matcher isAlphaNum = predicate(
|
||||||
|
(value) => value is String && _alphaNum.hasMatch(value), 'alphanumeric');
|
||||||
|
|
||||||
|
/// Asserts that a value either equals `true` or `false`.
|
||||||
|
final Matcher isBool = predicate((value) => value is bool, 'a bool');
|
||||||
|
|
||||||
|
/// Asserts that a `String` complies to the RFC 5322 e-mail standard.
|
||||||
|
final Matcher isEmail = predicate(
|
||||||
|
(value) => value is String && _email.hasMatch(value),
|
||||||
|
'a valid e-mail address');
|
||||||
|
|
||||||
|
/// Asserts that a value is an `int`.
|
||||||
|
final Matcher isInt = predicate((value) => value is int, 'an integer');
|
||||||
|
|
||||||
|
/// Asserts that a value is a `num`.
|
||||||
|
final Matcher isNum = predicate((value) => value is num, 'a number');
|
||||||
|
|
||||||
|
/// Asserts that a value is a `String`.
|
||||||
|
final Matcher isString = predicate((value) => value is String, 'a string');
|
||||||
|
|
||||||
|
/// Asserts that a value is a non-empty `String`.
|
||||||
|
final Matcher isNonEmptyString = predicate(
|
||||||
|
(value) => value is String && value.trim().isNotEmpty,
|
||||||
|
'a non-empty string');
|
||||||
|
|
||||||
|
/// Asserts that a value, presumably from a checkbox, is positive.
|
||||||
|
final Matcher isChecked =
|
||||||
|
isIn(const ['yes', 'checked', 'on', '1', 1, 1.0, true, 'true']);
|
||||||
|
|
||||||
|
/// Ensures that a string is an ISO-8601 date string.
|
||||||
|
final Matcher isIso8601DateString = predicate(
|
||||||
|
(x) {
|
||||||
|
try {
|
||||||
|
return x is String && DateTime.parse(x) != null;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'a valid ISO-8601 date string.',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Asserts that a `String` is an `http://` or `https://` URL.
|
||||||
|
///
|
||||||
|
/// The regular expression used:
|
||||||
|
/// ```
|
||||||
|
/// https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)
|
||||||
|
/// ```
|
||||||
|
final Matcher isUrl = predicate(
|
||||||
|
(value) => value is String && _url.hasMatch(value),
|
||||||
|
'a valid url, starting with http:// or https://');
|
||||||
|
|
||||||
|
/// Enforces a minimum length on a string.
|
||||||
|
Matcher minLength(int length) => predicate(
|
||||||
|
(value) => value is String && value.length >= length,
|
||||||
|
'a string at least $length character(s) long');
|
||||||
|
|
||||||
|
/// Limits the maximum length of a string.
|
||||||
|
Matcher maxLength(int length) => predicate(
|
||||||
|
(value) => value is String && value.length <= length,
|
||||||
|
'a string no longer than $length character(s) long');
|
Loading…
Reference in a new issue