Form class

This commit is contained in:
Tobe O 2019-10-16 20:13:36 -04:00
parent 8f025080ac
commit b50102d611
5 changed files with 153 additions and 10 deletions

View file

@ -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);
}
}

View file

@ -1,2 +0,0 @@
export 'src/field.dart';
export 'src/form.dart';

4
lib/angel_validate.dart Normal file
View file

@ -0,0 +1,4 @@
export 'src/field.dart';
export 'src/form.dart';
export 'src/form_renderer.dart';
export 'src/matchers.dart';

View file

@ -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
View 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');