Merge branch 'mbruce/textArea-refactor-entryfield' into 'develop'

Refactored to use validator

See merge request BPHV_MIPS/vds_ios!192
This commit is contained in:
Bruce, Matt R 2024-03-26 17:50:05 +00:00
commit 0289764065
4 changed files with 118 additions and 25 deletions

View File

@ -13,7 +13,7 @@ import Combine
/// Base Class used to build out a Input controls.
@objc(VDSEntryField)
open class EntryFieldBase: Control, Changeable, FormFieldable {
open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//--------------------------------------------------
// MARK: - Initializers
@ -153,8 +153,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldable {
/// Whether not to show the error.
open var showError: Bool = false { didSet { setNeedsUpdate() } }
/// FormFieldValidator
internal var validator: (any FormFieldValidatorable)?
/// Whether or not to show the internal error
open internal(set) var hasInternalError: Bool = false { didSet { setNeedsUpdate() } }
open var hasInternalError: Bool { !(validator?.isValid ?? true) }
/// Override UIControl state to add the .error state if showError is true.
open override var state: UIControl.State {
@ -175,7 +178,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldable {
}
}
internal var internalErrorText: String? {
open var internalErrorText: String? {
didSet {
updateContainerView()
updateErrorLabel()
@ -200,19 +203,18 @@ open class EntryFieldBase: Control, Changeable, FormFieldable {
open var inputId: String? { didSet { setNeedsUpdate() } }
/// The text of this textField.
private var _value: AnyHashable?
open var value: AnyHashable? {
private var _value: String?
open var value: String? {
get { _value }
set {
if let newValue, newValue != _value {
_value = newValue
text = newValue as? String
text = newValue
}
setNeedsUpdate()
}
}
open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } }
open var required: Bool = false { didSet { setNeedsUpdate() } }
@ -324,6 +326,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldable {
updateHelperLabel()
backgroundColor = surface.color
validator?.validate()
internalErrorText = validator?.errorMessage
}
//--------------------------------------------------

View File

@ -89,7 +89,7 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
setNeedsUpdate()
}
}
var _showError: Bool = false
/// Whether not to show the error.
open override var showError: Bool {

View File

@ -107,17 +107,19 @@ open class TextArea: EntryFieldBase {
}
/// The text of this textView
private var _text: String?
open override var text: String? {
get { textView.text }
set {
if let newValue, newValue != text {
if let newValue, newValue != _text {
_text = newValue
textView.text = newValue
value = newValue
}
setNeedsUpdate()
}
}
/// UITextView shown in the TextArea.
open var textView = TextView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
@ -125,6 +127,8 @@ open class TextArea: EntryFieldBase {
$0.isScrollEnabled = false
}
open override var maxLength: Int? { willSet { countRule.maxLength = newValue }}
/// Color configuration for error icon.
internal var iconColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
@ -147,6 +151,7 @@ open class TextArea: EntryFieldBase {
open override func setup() {
super.setup()
accessibilityLabel = "TextArea"
validator = FormFieldValidator<TextArea>(field: self, rules: [.init(countRule)])
containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset))
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width)
@ -187,7 +192,6 @@ open class TextArea: EntryFieldBase {
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
textView.isEditable = isEnabled
textView.isEnabled = isEnabled
textView.surface = surface
@ -202,13 +206,8 @@ open class TextArea: EntryFieldBase {
widthConstraint?.isActive = false
minWidthConstraint?.isActive = true
}
let characterError = getCharacterCounterText()
if let maxLength, maxLength > 0 {
characterCounterLabel.text = characterError
} else {
characterCounterLabel.text = ""
}
characterCounterLabel.text = getCharacterCounterText()
icon.size = .medium
icon.color = iconColorConfiguration.getColor(self)
@ -247,17 +246,11 @@ open class TextArea: EntryFieldBase {
let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)"
if let maxLength, maxLength > 0 {
if count > maxLength {
hasInternalError = true
internalErrorText = "You have exceeded the character limit."
return countStr
} else {
hasInternalError = false
internalErrorText = nil
return ("\(countStr)" + "/" + "\(maxLength)")
}
} else {
hasInternalError = false
internalErrorText = nil
return nil
}
}
@ -277,6 +270,21 @@ open class TextArea: EntryFieldBase {
textView.textAttributes = textAttributes
}
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
var countRule = CharacterCountRule()
class CharacterCountRule: Rule {
var maxLength: Int?
var errorMessage: String = "You have exceeded the character limit."
func isValid(value: String?) -> Bool {
guard let text = value, let maxLength, maxLength > 0 else { return true }
return text.count <= maxLength
}
}
}
extension TextArea: UITextViewDelegate {

View File

@ -9,10 +9,91 @@ import Foundation
/// Protocol used for a FormField object.
public protocol FormFieldable {
associatedtype ValueType = AnyHashable
/// Unique Id for the Form Field object within a Form.
var inputId: String? { get set }
/// Value for the Form Field.
var value: AnyHashable? { get set }
var value: ValueType? { get set }
}
/// Protocol for FormFieldable that require internal validation.
public protocol FormFieldInternalValidatable: FormFieldable {
/// Is there an internalError
var hasInternalError: Bool { get }
/// Internal Error Message that will show.
var internalErrorText: String? { get }
}
/// Struct that will execute the validation.
public protocol FormFieldValidatorable {
associatedtype FieldType: FormFieldable
/// FormFieldable to be validated.
var field: FieldType { get set }
/// Rules that will be applied against the FormFieldable
var rules: [AnyRule<FieldType.ValueType>] { get set }
/// Error Message that will show.
var errorMessage: String? { get }
/// Is the FormField valid.
var isValid: Bool { get }
/// Run the rules against the FormFieldable.
func validate()
}
/// Rule that will be executed against a specific ValueType.
public protocol Rule<ValueType> {
associatedtype ValueType
/// Determines if this rule valid for the value passed.
func isValid(value: ValueType?) -> Bool
/// Error Message to be show if the value is invalid.
var errorMessage: String { get }
}
/// Type Erased Rule for a specific ValueType.
public struct AnyRule<ValueType>: Rule {
private let _isValid: (ValueType?) -> Bool
public let errorMessage: String
public init<R: Rule>(_ rule: R) where R.ValueType == ValueType {
self._isValid = rule.isValid
self.errorMessage = rule.errorMessage
}
public func isValid(value: ValueType?) -> Bool {
return _isValid(value)
}
}
/// Generic Validator for a specific FormFieldable.
public class FormFieldValidator<Field:FormFieldable>: FormFieldValidatorable{
public var field: Field
public var rules: [AnyRule<Field.ValueType>]
public var errorMessages = [String]()
public var isValid: Bool = true
public init(field: Field, rules: [AnyRule<Field.ValueType>]) {
self.field = field
self.rules = rules
}
public var errorMessage: String? {
guard errorMessages.count > 0 else { return nil }
return errorMessages.joined(separator: "\r")
}
public func validate() {
errorMessages.removeAll()
for rule in rules {
if !rule.isValid(value: field.value) {
errorMessages.append(rule.errorMessage)
isValid = false
return
}
}
isValid = true
}
}