From cebdb22dec8cc235056dd85de7deeeeb6113c9cd Mon Sep 17 00:00:00 2001 From: TheEthicalBoy <98978078+ndonkoHenri@users.noreply.github.com> Date: Mon, 20 Nov 2023 21:01:55 +0100 Subject: [PATCH] Feature: `TextField` Input validation (#2101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add ´InputFilter´ * create textfield utils * update textfield.dart * update return type * remove `regex_string` default * add predefined InputFilters --- package/lib/src/controls/textfield.dart | 21 ++++++++--- package/lib/src/utils/textfield.dart | 36 +++++++++++++++++++ .../flet-core/src/flet_core/__init__.py | 9 ++++- .../flet-core/src/flet_core/textfield.py | 29 +++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 package/lib/src/utils/textfield.dart diff --git a/package/lib/src/controls/textfield.dart b/package/lib/src/controls/textfield.dart index 288fdfc81..4845ede29 100644 --- a/package/lib/src/controls/textfield.dart +++ b/package/lib/src/controls/textfield.dart @@ -10,6 +10,7 @@ import '../protocol/update_control_props_payload.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/text.dart'; +import '../utils/textfield.dart'; import 'create_control.dart'; import 'form_field.dart'; @@ -162,6 +163,19 @@ class _TextFieldControlState extends State { .toLowerCase(), orElse: () => TextCapitalization.none); + FilteringTextInputFormatter? inputFilter = + parseInputFilter(widget.control, "inputFilter"); + + List? inputFormatters = []; + // add non-null input formatters + if (inputFilter != null) { + inputFormatters.add(inputFilter); + } + if (textCapitalization != TextCapitalization.none) { + inputFormatters + .add(TextCapitalizationFormatter(textCapitalization)); + } + Widget? revealPasswordIcon; if (password && canRevealPassword) { revealPasswordIcon = GestureDetector( @@ -241,11 +255,8 @@ class _TextFieldControlState extends State { maxLines: maxLines, maxLength: maxLength, readOnly: readOnly, - inputFormatters: textCapitalization != TextCapitalization.none - ? [ - TextCapitalizationFormatter(textCapitalization), - ] - : null, + inputFormatters: + inputFormatters.isNotEmpty ? inputFormatters : null, obscureText: password && !_revealPassword, controller: _controller, focusNode: focusNode, diff --git a/package/lib/src/utils/textfield.dart b/package/lib/src/utils/textfield.dart new file mode 100644 index 000000000..fb35d25e2 --- /dev/null +++ b/package/lib/src/utils/textfield.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:flet/src/utils/numbers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../models/control.dart'; + +FilteringTextInputFormatter? parseInputFilter( + Control control, String propName) { + var v = control.attrString(propName, null); + if (v == null) { + return null; + } + + final j1 = json.decode(v); + return inputFilterFromJSON(j1); +} + +FilteringTextInputFormatter inputFilterFromJSON(dynamic json) { + bool allow = true; + String? regexString = ""; + String? replacementString = ""; + + if (json != null) { + allow = parseBool(json["allow"], true); + regexString = json["regex_string"]?.toString(); + replacementString = json["replacement_string"]?.toString(); + } + + debugPrint( + "Textfield inputFilter - allow: $allow | regexString: $regexString | replacementString: $replacementString"); + + return FilteringTextInputFormatter(RegExp(regexString ?? ""), + allow: allow, replacementString: replacementString ?? ""); +} diff --git a/sdk/python/packages/flet-core/src/flet_core/__init__.py b/sdk/python/packages/flet-core/src/flet_core/__init__.py index 3e9b51769..be5790700 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -169,7 +169,14 @@ from flet_core.text_button import TextButton from flet_core.text_span import TextSpan from flet_core.text_style import TextDecoration, TextDecorationStyle, TextStyle -from flet_core.textfield import KeyboardType, TextCapitalization, TextField +from flet_core.textfield import ( + KeyboardType, + InputFilter, + NumbersOnlyInputFilter, + TextCapitalization, + TextField, + TextOnlyInputFilter, +) from flet_core.theme import ( ColorScheme, PageTransitionsTheme, diff --git a/sdk/python/packages/flet-core/src/flet_core/textfield.py b/sdk/python/packages/flet-core/src/flet_core/textfield.py index 7a8c72266..2c5808d55 100644 --- a/sdk/python/packages/flet-core/src/flet_core/textfield.py +++ b/sdk/python/packages/flet-core/src/flet_core/textfield.py @@ -1,3 +1,5 @@ +import dataclasses +from dataclasses import field import time from enum import Enum from typing import Any, Optional, Union @@ -63,6 +65,21 @@ class TextCapitalization(Enum): SENTENCES = "sentences" +@dataclasses.dataclass +class InputFilter: + regex_string: str + allow: bool = field(default=True) + replacement_string: str = field(default="") + +class NumbersOnlyInputFilter(InputFilter): + def __init__(self): + super().__init__(r"[0-9]") + +class TextOnlyInputFilter(InputFilter): + def __init__(self): + super().__init__(r"[a-zA-Z]") + + class TextField(FormFieldControl): """ A text field lets the user enter text, either with hardware keyboard or with an onscreen keyboard. @@ -179,6 +196,7 @@ def __init__( cursor_height: OptionalNumber = None, cursor_radius: OptionalNumber = None, selection_color: Optional[str] = None, + input_filter: Optional[InputFilter] = None, on_change=None, on_submit=None, on_focus=None, @@ -269,6 +287,7 @@ def __init__( self.cursor_width = cursor_width self.cursor_radius = cursor_radius self.selection_color = selection_color + self.input_filter = input_filter self.on_change = on_change self.on_submit = on_submit self.on_focus = on_focus @@ -279,6 +298,7 @@ def _get_control_name(self): def _before_build_command(self): super()._before_build_command() + self._set_attr_json("inputFilter", self.__input_filter) if self.bgcolor is not None and self.filled is None: self.filled = True # Flutter requires filled = True to display a bgcolor @@ -509,6 +529,15 @@ def selection_color(self): def selection_color(self, value): self._set_attr("selectionColor", value) + # input_filter + @property + def input_filter(self) -> Optional[InputFilter]: + return self.__input_filter + + @input_filter.setter + def input_filter(self, value: Optional[InputFilter]): + self.__input_filter = value + # on_change @property def on_change(self):