|
1 | 1 | import 'package:codelessly_json_annotation/codelessly_json_annotation.dart'; |
2 | 2 | import 'package:equatable/equatable.dart'; |
| 3 | +import 'package:meta/meta.dart' hide required; |
3 | 4 |
|
| 5 | +import '../extensions.dart'; |
4 | 6 | import '../mixins.dart'; |
5 | 7 |
|
6 | 8 | part 'text_input_validator_model.g.dart'; |
7 | 9 |
|
8 | | -/// Text formatters that can be applied to the text field input to restrict the |
9 | | -/// input to a specific format. |
| 10 | +/// Represents the type of validator. |
| 11 | +enum TextInputValidatorType { |
| 12 | + /// Represents absence of any validation. |
| 13 | + none, |
| 14 | + |
| 15 | + /// Represents a regular expression based text input validator. |
| 16 | + regex, |
| 17 | + |
| 18 | + /// Represents a validator for URL input. |
| 19 | + url, |
| 20 | +} |
| 21 | + |
| 22 | +/// Represents a text input validator. |
| 23 | +sealed class TextInputValidatorModel with EquatableMixin, SerializableMixin { |
| 24 | + /// The name of the validator. This is used to identify the validator. |
| 25 | + final String name; |
| 26 | + |
| 27 | + /// Whether the input is required. Defaults to `false`. If `true`, the input |
| 28 | + /// must not be empty. |
| 29 | + final bool required; |
| 30 | + |
| 31 | + /// The type of validator. |
| 32 | + @JsonKey(includeToJson: true, required: true) |
| 33 | + final TextInputValidatorType type; |
| 34 | + |
| 35 | + /// Creates a new [TextInputValidatorModel] instance. |
| 36 | + const TextInputValidatorModel({ |
| 37 | + required this.name, |
| 38 | + this.required = false, |
| 39 | + required this.type, |
| 40 | + }); |
| 41 | + |
| 42 | + /// Creates a [TextInputValidatorModel] instance from a JSON object. |
| 43 | + factory TextInputValidatorModel.fromJson(Map<String, dynamic> json) { |
| 44 | + final TextInputValidatorType type = |
| 45 | + TextInputValidatorType.values.byName(json['type']); |
| 46 | + return switch (type) { |
| 47 | + TextInputValidatorType.none => NoneTextInputValidatorModel.fromJson(json), |
| 48 | + TextInputValidatorType.regex => |
| 49 | + RegexTextInputValidatorModel.fromJson(json), |
| 50 | + TextInputValidatorType.url => UrlTextInputValidatorModel.fromJson(json) |
| 51 | + }; |
| 52 | + } |
| 53 | + |
| 54 | + /// Allows objects of this class and its subclasses to be used as a function. |
| 55 | + @internal |
| 56 | + String? call(String? input) => validate(input); |
| 57 | + |
| 58 | + /// Validates the input and returns an error message if the input is invalid. |
| 59 | + /// Returns `null` if the input is valid. |
| 60 | + /// This method should be overridden by the subclasses. |
| 61 | + String? validate(String? input); |
| 62 | + |
| 63 | + @override |
| 64 | + List<Object?> get props => [name, type, required]; |
| 65 | + |
| 66 | + /// A complete list of built-in validators. |
| 67 | + static const List<TextInputValidatorModel> validators = [ |
| 68 | + NoneTextInputValidatorModel(), |
| 69 | + ...RegexTextInputValidatorModel.validators, |
| 70 | + UrlTextInputValidatorModel(), |
| 71 | + ]; |
| 72 | +} |
| 73 | + |
| 74 | +/// Represents absence of any validation. |
| 75 | +// TODO: should this be removed and the validator field be nullable? |
| 76 | +@JsonSerializable() |
| 77 | +class NoneTextInputValidatorModel extends TextInputValidatorModel { |
| 78 | + /// Creates a new [NoneTextInputValidatorModel] instance. |
| 79 | + const NoneTextInputValidatorModel() |
| 80 | + : super(name: 'None', type: TextInputValidatorType.none); |
| 81 | + |
| 82 | + @override |
| 83 | + String? validate(String? input) => null; |
| 84 | + |
| 85 | + /// Creates a [TextInputValidatorModel] instance from a JSON object. |
| 86 | + factory NoneTextInputValidatorModel.fromJson(Map<String, dynamic> json) => |
| 87 | + _$NoneTextInputValidatorModelFromJson(json); |
| 88 | + |
| 89 | + @override |
| 90 | + Map toJson() => _$NoneTextInputValidatorModelToJson(this); |
| 91 | +} |
| 92 | + |
| 93 | +/// Represents the type of validation to perform. |
| 94 | +enum RegexValidationType { |
| 95 | + /// Allow the input if it matches the pattern. |
| 96 | + allow, |
| 97 | + |
| 98 | + /// Deny the input if it matches the pattern. |
| 99 | + deny, |
| 100 | +} |
| 101 | + |
| 102 | +/// Represents a regular expression based text input validator. |
10 | 103 | @JsonSerializable() |
11 | | -class TextInputValidatorModel with EquatableMixin, SerializableMixin { |
| 104 | +class RegexTextInputValidatorModel extends TextInputValidatorModel { |
12 | 105 | /// The regular expression to match the text. |
13 | 106 | final String pattern; |
14 | 107 |
|
15 | | - /// The name of the formatter. |
16 | | - final String name; |
17 | | - |
18 | | - /// The error message to show when the input is invalid. |
| 108 | + /// The error message to show when the input is invalid. Override the default |
| 109 | + /// [validate] method to have different/multiple error messages on different |
| 110 | + /// conditions. |
19 | 111 | final String errorMessage; |
20 | 112 |
|
21 | | - /// Allow only digits in the text field. |
22 | | - static const TextInputValidatorModel none = TextInputValidatorModel( |
23 | | - name: 'None', |
24 | | - pattern: r'.*', |
25 | | - errorMessage: '', |
26 | | - ); |
| 113 | + /// Whether the pattern matching should be case sensitive. |
| 114 | + /// Defaults to `true`. |
| 115 | + final bool caseSensitive; |
| 116 | + |
| 117 | + /// Whether the `.` pattern should match all characters, including |
| 118 | + /// line terminators. |
| 119 | + /// Defaults to `false`. |
| 120 | + final bool dotAll; |
| 121 | + |
| 122 | + /// Whether the pattern should match across multiple lines. |
| 123 | + /// Defaults to `false`. |
| 124 | + final bool multiLine; |
| 125 | + |
| 126 | + /// Whether the pattern should match unicode characters. |
| 127 | + /// Defaults to `false`. |
| 128 | + final bool unicode; |
| 129 | + |
| 130 | + /// The type of validation to perform. Default is [RegexValidationType.allow]. |
| 131 | + final RegexValidationType validationType; |
27 | 132 |
|
28 | 133 | /// Allow only digits in the text field. |
29 | | - static const TextInputValidatorModel digitsOnly = TextInputValidatorModel( |
| 134 | + static const RegexTextInputValidatorModel digitsOnly = |
| 135 | + RegexTextInputValidatorModel( |
30 | 136 | name: 'Digits Only', |
31 | 137 | pattern: r'[0-9]', |
32 | 138 | errorMessage: 'Only digits are allowed.', |
33 | 139 | ); |
34 | 140 |
|
35 | | - /// List of all available text field formatters. |
36 | | - static const List<TextInputValidatorModel> values = [ |
37 | | - none, |
38 | | - digitsOnly, |
39 | | - ]; |
| 141 | + /// Allow only alphabets in the text field. |
| 142 | + static const RegexTextInputValidatorModel alphabetsOnly = |
| 143 | + RegexTextInputValidatorModel( |
| 144 | + name: 'Alphabets Only', |
| 145 | + pattern: r'[a-zA-Z]', |
| 146 | + errorMessage: 'Only alphabets are allowed.', |
| 147 | + ); |
40 | 148 |
|
41 | | - /// Returns true if the formatter is [none]. |
42 | | - bool get isNone => this == none; |
| 149 | + /// Allow only alphabets and digits in the text field. |
| 150 | + static const RegexTextInputValidatorModel alphanumeric = |
| 151 | + RegexTextInputValidatorModel( |
| 152 | + name: 'Alphanumeric', |
| 153 | + pattern: r'[a-zA-Z0-9]', |
| 154 | + errorMessage: 'Only alphabets and digits are allowed.', |
| 155 | + ); |
43 | 156 |
|
44 | | - /// Creates a new [TextInputValidatorModel] instance with the given pattern. |
45 | | - const TextInputValidatorModel({ |
46 | | - required this.name, |
| 157 | + /// A validator for email addresses. |
| 158 | + static const RegexTextInputValidatorModel email = |
| 159 | + RegexTextInputValidatorModel( |
| 160 | + name: 'Email', |
| 161 | + pattern: r'^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$', |
| 162 | + errorMessage: 'Invalid email address.', |
| 163 | + ); |
| 164 | + |
| 165 | + /// A validator for phone numbers. |
| 166 | + static const RegexTextInputValidatorModel phoneNumber = |
| 167 | + RegexTextInputValidatorModel( |
| 168 | + name: 'Phone Number', |
| 169 | + pattern: |
| 170 | + r'^\+?([0-9]{1,4})\)?[-. ]?([0-9]{1,4})[-. ]?([0-9]{1,4})[-. ]?([0-9]{1,4})$', |
| 171 | + errorMessage: 'Invalid phone number.', |
| 172 | + ); |
| 173 | + |
| 174 | + /// A list of built-in regex validators. |
| 175 | + static const List<RegexTextInputValidatorModel> validators = [ |
| 176 | + RegexTextInputValidatorModel.digitsOnly, |
| 177 | + RegexTextInputValidatorModel.alphabetsOnly, |
| 178 | + RegexTextInputValidatorModel.alphanumeric, |
| 179 | + RegexTextInputValidatorModel.email, |
| 180 | + RegexTextInputValidatorModel.phoneNumber, |
| 181 | + ]; |
| 182 | + |
| 183 | + /// Creates a new [RegexTextInputValidatorModel] instance with the given pattern. |
| 184 | + const RegexTextInputValidatorModel({ |
| 185 | + required super.name, |
47 | 186 | required this.pattern, |
48 | 187 | required this.errorMessage, |
49 | | - }); |
| 188 | + this.caseSensitive = true, |
| 189 | + this.dotAll = false, |
| 190 | + this.multiLine = false, |
| 191 | + this.unicode = false, |
| 192 | + this.validationType = RegexValidationType.allow, |
| 193 | + super.required, |
| 194 | + }) : super(type: TextInputValidatorType.regex); |
50 | 195 |
|
51 | | - /// Validates the input against the pattern. This can be used to use |
52 | | - /// this class as a function. |
53 | | - String? call(String? value) => validate(value); |
54 | | - |
55 | | - /// Validates the input against the pattern. |
| 196 | + @override |
56 | 197 | String? validate(String? input) { |
57 | | - // check if the input perfectly matches the pattern. |
58 | | - if (RegExp(pattern).firstMatch(input ?? '') |
59 | | - case RegExpMatch(start: var start, end: var end)) { |
60 | | - if (start == 0 && end == (input?.length ?? 0)) { |
61 | | - // A perfect match. This is valid. |
62 | | - return null; |
63 | | - } |
| 198 | + if (input == null) { |
| 199 | + if (required) return 'This field is required.'; |
| 200 | + return null; |
64 | 201 | } |
| 202 | + final bool hasExactMatch = regex.hasExactMatch(input); |
| 203 | + final bool isValid = switch (validationType) { |
| 204 | + RegexValidationType.allow => hasExactMatch, |
| 205 | + RegexValidationType.deny => !hasExactMatch, |
| 206 | + }; |
| 207 | + |
| 208 | + if (isValid) return null; |
65 | 209 | return errorMessage; |
66 | 210 | } |
67 | 211 |
|
68 | | - /// copyWith |
69 | | - TextInputValidatorModel copyWith({ |
| 212 | + /// Builds a [RegExp] instance from the [pattern] and other properties like |
| 213 | + /// [caseSensitive], [dotAll], [multiLine], and [unicode]. |
| 214 | + RegExp get regex => RegExp( |
| 215 | + pattern, |
| 216 | + caseSensitive: caseSensitive, |
| 217 | + dotAll: dotAll, |
| 218 | + multiLine: multiLine, |
| 219 | + unicode: unicode, |
| 220 | + ); |
| 221 | + |
| 222 | + /// Creates a copy of this instance with the given properties replaced. |
| 223 | + RegexTextInputValidatorModel copyWith({ |
70 | 224 | String? name, |
71 | 225 | String? pattern, |
72 | 226 | String? errorMessage, |
| 227 | + bool? caseSensitive, |
| 228 | + bool? dotAll, |
| 229 | + bool? multiLine, |
| 230 | + bool? unicode, |
| 231 | + RegexValidationType? validationType, |
| 232 | + bool? required, |
73 | 233 | }) { |
74 | | - return TextInputValidatorModel( |
| 234 | + return RegexTextInputValidatorModel( |
75 | 235 | name: name ?? this.name, |
76 | 236 | pattern: pattern ?? this.pattern, |
77 | 237 | errorMessage: errorMessage ?? this.errorMessage, |
| 238 | + caseSensitive: caseSensitive ?? this.caseSensitive, |
| 239 | + dotAll: dotAll ?? this.dotAll, |
| 240 | + multiLine: multiLine ?? this.multiLine, |
| 241 | + unicode: unicode ?? this.unicode, |
| 242 | + validationType: validationType ?? this.validationType, |
| 243 | + required: required ?? this.required, |
78 | 244 | ); |
79 | 245 | } |
80 | 246 |
|
81 | 247 | /// Creates a [TextInputValidatorModel] instance from a JSON object. |
82 | | - factory TextInputValidatorModel.fromJson(Map json) => |
83 | | - _$TextInputValidatorModelFromJson(json); |
| 248 | + factory RegexTextInputValidatorModel.fromJson(Map json) => |
| 249 | + _$RegexTextInputValidatorModelFromJson(json); |
| 250 | + |
| 251 | + @override |
| 252 | + Map toJson() => _$RegexTextInputValidatorModelToJson(this); |
| 253 | + |
| 254 | + @override |
| 255 | + List<Object?> get props => [ |
| 256 | + ...super.props, |
| 257 | + pattern, |
| 258 | + errorMessage, |
| 259 | + caseSensitive, |
| 260 | + dotAll, |
| 261 | + multiLine, |
| 262 | + unicode, |
| 263 | + validationType, |
| 264 | + ]; |
| 265 | +} |
| 266 | + |
| 267 | +/// A custom validator that takes a callback function. |
| 268 | +@JsonSerializable() |
| 269 | +class UrlTextInputValidatorModel extends TextInputValidatorModel { |
| 270 | + /// Custom error message to show when the input is invalid. |
| 271 | + final String? errorMessage; |
| 272 | + |
| 273 | + /// Creates a new [UrlTextInputValidatorModel] instance. |
| 274 | + const UrlTextInputValidatorModel({ |
| 275 | + this.errorMessage, |
| 276 | + super.required, |
| 277 | + }) : super( |
| 278 | + name: 'URL', |
| 279 | + type: TextInputValidatorType.url, |
| 280 | + ); |
| 281 | + |
| 282 | + @override |
| 283 | + String? validate(String? input) { |
| 284 | + if (input == null) { |
| 285 | + if (required) return 'This field is required.'; |
| 286 | + return null; |
| 287 | + } |
| 288 | + if (Uri.tryParse(input)?.isAbsolute == true) { |
| 289 | + return null; |
| 290 | + } |
| 291 | + return errorMessage ?? 'Invalid URL.'; |
| 292 | + } |
| 293 | + |
| 294 | + /// Creates a [TextInputValidatorModel] instance from a JSON object. |
| 295 | + factory UrlTextInputValidatorModel.fromJson(Map json) => |
| 296 | + _$UrlTextInputValidatorModelFromJson(json); |
84 | 297 |
|
85 | 298 | @override |
86 | | - Map toJson() => _$TextInputValidatorModelToJson(this); |
| 299 | + Map toJson() => _$UrlTextInputValidatorModelToJson(this); |
87 | 300 |
|
88 | 301 | @override |
89 | | - List<Object?> get props => [name]; |
| 302 | + List<Object?> get props => [...super.props, errorMessage]; |
90 | 303 | } |
0 commit comments