Merge branch 'feature/atomic-vds-dropDownSelect' into 'develop'
VDS Brand 3.0 Dropdown Select ### Summary VDS Brand 3.0 Dropdown Select for IOS ### JIRA Ticket https://onejira.verizon.com/browse/ONEAPP-7135 Co-authored-by: Matt Bruce <matt.bruce@verizon.com> See merge request https://gitlab.verizon.com/BPHV_MIPS/mvm_core_ui/-/merge_requests/1157
This commit is contained in:
commit
fbecc82d20
@ -581,9 +581,11 @@
|
||||
EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; };
|
||||
EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; };
|
||||
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */; };
|
||||
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */; };
|
||||
EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; };
|
||||
EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; };
|
||||
EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; };
|
||||
EA5DBDAB2C35B6C500290DF8 /* FormFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5DBDAA2C35B6C500290DF8 /* FormFieldModel.swift */; };
|
||||
EA6642912BCDA97300D81DC4 /* TileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6642902BCDA97300D81DC4 /* TileContainer.swift */; };
|
||||
EA6642932BCDA97D00D81DC4 /* TileContainerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */; };
|
||||
EA6E8B952B504A43000139B4 /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6E8B942B504A43000139B4 /* ButtonGroup.swift */; };
|
||||
@ -1204,9 +1206,11 @@
|
||||
EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicatorModel.swift; sourceTree = "<group>"; };
|
||||
EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = "<group>"; };
|
||||
EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
|
||||
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleVDSModel.swift; sourceTree = "<group>"; };
|
||||
EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
||||
EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = "<group>"; };
|
||||
EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = "<group>"; };
|
||||
EA5DBDAA2C35B6C500290DF8 /* FormFieldModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormFieldModel.swift; sourceTree = "<group>"; };
|
||||
EA6642902BCDA97300D81DC4 /* TileContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainer.swift; sourceTree = "<group>"; };
|
||||
EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainerModel.swift; sourceTree = "<group>"; };
|
||||
EA6E8B942B504A43000139B4 /* ButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroup.swift; sourceTree = "<group>"; };
|
||||
@ -1300,6 +1304,7 @@
|
||||
011D95A0240453D0000E3791 /* RuleEqualsModel.swift */,
|
||||
0A849EFD246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift */,
|
||||
FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */,
|
||||
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */,
|
||||
);
|
||||
name = Rules;
|
||||
path = Rules/Rules;
|
||||
@ -2496,6 +2501,7 @@
|
||||
D2BEFEF5248A954C00FAB3A9 /* FormFields */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA5DBDAA2C35B6C500290DF8 /* FormFieldModel.swift */,
|
||||
D2BEFEF6248A957A00FAB3A9 /* Tags */,
|
||||
D29DF22B21E6A0FA003B2FB9 /* TextFields */,
|
||||
);
|
||||
@ -2842,6 +2848,7 @@
|
||||
D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */,
|
||||
EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */,
|
||||
011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */,
|
||||
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */,
|
||||
EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */,
|
||||
BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */,
|
||||
D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */,
|
||||
@ -3304,6 +3311,7 @@
|
||||
D28BA7432480284E00B75CB8 /* TabBar.swift in Sources */,
|
||||
AA3561AE24C96B9000452EB1 /* ListRightVariableRightCaretAllTextAndLinks.swift in Sources */,
|
||||
AA26850C244840AE00CE34CC /* HeadersH2TinyButton.swift in Sources */,
|
||||
EA5DBDAB2C35B6C500290DF8 /* FormFieldModel.swift in Sources */,
|
||||
011D95AB2405C553000E3791 /* FormItemProtocol.swift in Sources */,
|
||||
D21EE53C23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift in Sources */,
|
||||
0A25209824645B76000FA9F6 /* TextViewEntryFieldModel.swift in Sources */,
|
||||
|
||||
134
MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift
Normal file
134
MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift
Normal file
@ -0,0 +1,134 @@
|
||||
//
|
||||
// FormFieldModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 7/3/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
@objcMembers open class FormFieldModel: MoleculeModelProtocol, FormFieldProtocol, FormRuleWatcherFieldProtocol, UIUpdatableModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public class var identifier: String { "" }
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var required: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var showError: Bool?
|
||||
public var errorMessage: String?
|
||||
public var initialErrorMessage: String?
|
||||
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var baseValue: AnyHashable?
|
||||
|
||||
public var inverted: Bool = false
|
||||
public var surface: Surface { inverted ? .dark : .light }
|
||||
|
||||
public var dynamicErrorMessage: String? {
|
||||
didSet {
|
||||
isValid = dynamicErrorMessage?.isEmpty ?? true
|
||||
updateUIDynamicError?()
|
||||
}
|
||||
}
|
||||
|
||||
public var isValid: Bool? = true {
|
||||
didSet { updateUI?() }
|
||||
}
|
||||
|
||||
/// Temporary binding mechanism for the view to update on enable changes.
|
||||
public var updateUI: ActionBlock?
|
||||
|
||||
// TODO: Remove once updateUI is fixed with isSelected
|
||||
public var updateUIDynamicError: ActionBlock?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init() {}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case errorMessage
|
||||
case enabled
|
||||
case readOnly
|
||||
case required
|
||||
case fieldKey
|
||||
case groupName
|
||||
case inverted
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
open func formFieldValue() -> AnyHashable? {
|
||||
fatalError("developer must implement")
|
||||
}
|
||||
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
}
|
||||
|
||||
public func setValidity(_ valid: Bool, errorMessage: String?) {
|
||||
|
||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||
self.errorMessage = ruleErrorMessage
|
||||
} else {
|
||||
self.errorMessage = initialErrorMessage
|
||||
}
|
||||
|
||||
isValid = valid
|
||||
updateUI?()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codable
|
||||
//--------------------------------------------------
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||
initialErrorMessage = errorMessage
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) ?? FormValidator.defaultGroupName
|
||||
|
||||
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||
self.inverted = inverted
|
||||
}
|
||||
}
|
||||
|
||||
open func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(errorMessage, forKey: .errorMessage)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(required, forKey: .required)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
}
|
||||
}
|
||||
@ -7,19 +7,66 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, ObservingTextFieldDelegate {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: ItemDropdownEntryFieldModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
open var pickerData: [String] = [] {
|
||||
didSet {
|
||||
options = pickerData.compactMap({ DropdownOptionModel(text: $0) })
|
||||
}
|
||||
}
|
||||
|
||||
private var isEditting: Bool = false
|
||||
|
||||
open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public var isValid: Bool = true
|
||||
|
||||
/// Closure passed here will run as picker changes items.
|
||||
public var observeDropdownChange: ((String?, String) -> ())?
|
||||
|
||||
open var pickerData: [String] = []
|
||||
/// Closure passed here will run upon dismissing the selection picker.
|
||||
public var observeDropdownSelection: ((String) -> ())?
|
||||
|
||||
public var itemDropdownEntryFieldModel: ItemDropdownEntryFieldModel? {
|
||||
model as? ItemDropdownEntryFieldModel
|
||||
/// When selecting for first responder, allow initial selected value to appear in empty text field.
|
||||
public var setInitialValueInTextField = true
|
||||
|
||||
open override var errorText: String? {
|
||||
get {
|
||||
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||
}
|
||||
set {}
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Delegate Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||
public weak var observingTextFieldDelegate: ObservingTextFieldDelegate?
|
||||
|
||||
/// If you're using a ViewController, you must set this to it
|
||||
open weak var uiTextFieldDelegate: UITextFieldDelegate? {
|
||||
get { dropdownField.delegate }
|
||||
set { dropdownField.delegate = newValue }
|
||||
}
|
||||
|
||||
@objc public func dismissFieldInput(_ sender: Any?) {
|
||||
_ = resignFirstResponder()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -28,7 +75,7 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
@objc public convenience init() {
|
||||
@objc public convenience required init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
@ -40,76 +87,134 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
@objc required public init?(coder: NSCoder) {
|
||||
fatalError("ItemDropdownEntryField init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.init(model: model, delegateObject, additionalData)
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
useRequiredRule = false
|
||||
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self, let selectedItem else { return }
|
||||
viewModel.selectedIndex = control.selectId
|
||||
observeDropdownSelection?(selectedItem.text)
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}.store(in: &subscribers)
|
||||
|
||||
dropdownField
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] textField in
|
||||
guard let self else { return }
|
||||
isEditting = true
|
||||
setInitialValueFromPicker()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
dropdownField
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] textField in
|
||||
guard let self else { return }
|
||||
isEditting = false
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
if let valid = viewModel.isValid {
|
||||
updateValidation(valid)
|
||||
}
|
||||
performDropdownAction()
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
pickerData = viewModel.options
|
||||
showInlineLabel = viewModel.showInlineLabel
|
||||
helperTextPlacement = viewModel.feedbackTextPlacement
|
||||
labelText = viewModel.title
|
||||
helperText = viewModel.feedback
|
||||
isEnabled = viewModel.enabled
|
||||
isReadOnly = viewModel.readOnly
|
||||
isRequired = viewModel.required
|
||||
tooltipModel = viewModel.tooltip?.toVDSTooltipModel()
|
||||
width = viewModel.width
|
||||
transparentBackground = viewModel.transparentBackground
|
||||
|
||||
if let index = viewModel.selectedIndex {
|
||||
selectId = index
|
||||
optionsPicker.selectRow(index, inComponent: 0, animated: false)
|
||||
pickerView(optionsPicker, didSelectRow: index, inComponent: 0)
|
||||
}
|
||||
|
||||
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||
|
||||
viewModel.wasInitiallySelected = true
|
||||
isEditting = true
|
||||
}
|
||||
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
if isEditting {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if isEditting {
|
||||
updateValidation(viewModel.isValid ?? true)
|
||||
|
||||
} else if viewModel.isValid ?? true && showError {
|
||||
showError = false
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.updateUIDynamicError = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let validState = viewModel.isValid ?? false
|
||||
if !validState && viewModel.shouldClearText {
|
||||
selectId = nil
|
||||
viewModel.shouldClearText = false
|
||||
}
|
||||
updateValidation(validState)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func updateView(_ size: CGFloat) { }
|
||||
|
||||
/// Sets the textField with the first value of the available picker data.
|
||||
@objc private func setInitialValueFromPicker() {
|
||||
private func setInitialValueFromPicker() {
|
||||
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
if setInitialValueInTextField {
|
||||
let pickerIndex = pickerView.selectedRow(inComponent: 0)
|
||||
itemDropdownEntryFieldModel?.selectedIndex = pickerIndex
|
||||
observeDropdownChange?(text, pickerData[pickerIndex])
|
||||
text = pickerData[pickerIndex]
|
||||
let pickerIndex = optionsPicker.selectedRow(inComponent: 0)
|
||||
viewModel.selectedIndex = pickerIndex
|
||||
selectId = pickerIndex
|
||||
observeDropdownChange?(selectedItem?.text, pickerData[pickerIndex])
|
||||
}
|
||||
}
|
||||
|
||||
@objc override func startEditing() {
|
||||
super.startEditing()
|
||||
|
||||
setInitialValueFromPicker()
|
||||
private func performDropdownAction() {
|
||||
guard let actionModel = viewModel.action,
|
||||
!dropdownField.isFirstResponder
|
||||
else { return }
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
@objc override func endInputing() {
|
||||
super.endInputing()
|
||||
private func updateValidation(_ isValid: Bool) {
|
||||
let previousValidity = self.isValid
|
||||
self.isValid = isValid
|
||||
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
observeDropdownSelection?(pickerData[pickerView.selectedRow(inComponent: 0)])
|
||||
}
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
|
||||
guard let model = model as? ItemDropdownEntryFieldModel else { return }
|
||||
|
||||
pickerData = model.options
|
||||
|
||||
if let index = model.selectedIndex {
|
||||
self.pickerView.selectRow(index, inComponent: 0, animated: false)
|
||||
self.pickerView(pickerView, didSelectRow: index, inComponent: 0)
|
||||
if previousValidity && !isValid {
|
||||
showError = true
|
||||
} else if (!previousValidity && isValid) {
|
||||
showError = false
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Picker Delegate
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public override func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 }
|
||||
|
||||
@objc public override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||
pickerData.count
|
||||
}
|
||||
|
||||
@objc public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||
guard !pickerData.isEmpty else { return nil }
|
||||
|
||||
return pickerData[row]
|
||||
}
|
||||
|
||||
@objc public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
itemDropdownEntryFieldModel?.selectedIndex = row
|
||||
observeDropdownChange?(text, pickerData[row])
|
||||
text = pickerData[row]
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,16 +5,22 @@
|
||||
// Created by Kevin Christiano on 1/22/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
@objcMembers open class ItemDropdownEntryFieldModel: BaseItemPickerEntryFieldModel {
|
||||
@objcMembers open class ItemDropdownEntryFieldModel: TextEntryFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public override class var identifier: String { "dropDown" }
|
||||
|
||||
public var action: ActionModelProtocol?
|
||||
public var options: [String] = []
|
||||
public var selectedIndex: Int?
|
||||
public var showInlineLabel: Bool = false
|
||||
public var feedbackTextPlacement: VDS.EntryFieldBase.HelperTextPlacement = .bottom
|
||||
public var tooltip: TooltipModel?
|
||||
public var transparentBackground: Bool = false
|
||||
public var width: CGFloat?
|
||||
|
||||
public init(with options: [String], selectedIndex: Int? = nil) {
|
||||
self.options = options
|
||||
@ -42,6 +48,12 @@
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case options
|
||||
case selectedIndex
|
||||
case action
|
||||
case showInlineLabel
|
||||
case feedbackTextPlacement
|
||||
case tooltip
|
||||
case transparentBackground
|
||||
case width
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -58,6 +70,12 @@
|
||||
self.selectedIndex = selectedIndex
|
||||
baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil
|
||||
}
|
||||
showInlineLabel = try typeContainer.decodeIfPresent(Bool.self, forKey: .showInlineLabel) ?? false
|
||||
feedbackTextPlacement = try typeContainer.decodeIfPresent(VDS.EntryFieldBase.HelperTextPlacement.self, forKey: .feedbackTextPlacement) ?? .bottom
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip)
|
||||
transparentBackground = try typeContainer.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false
|
||||
width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
@ -65,5 +83,11 @@
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(options, forKey: .options)
|
||||
try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex)
|
||||
try container.encode(showInlineLabel, forKey: .showInlineLabel)
|
||||
try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
try container.encodeIfPresent(tooltip, forKey: .tooltip)
|
||||
try container.encode(transparentBackground, forKey: .transparentBackground)
|
||||
try container.encodeIfPresent(width, forKey: .width)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,79 +9,39 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
@objcMembers open class EntryFieldModel: MoleculeModelProtocol, FormFieldProtocol, FormRuleWatcherFieldProtocol, UIUpdatableModelProtocol, ClearableModelProtocol {
|
||||
@objcMembers open class EntryFieldModel: FormFieldModel, ClearableModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public class var identifier: String { "" }
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public var shouldClearText: Bool = false
|
||||
public var dynamicErrorMessage: String? {
|
||||
didSet {
|
||||
isValid = dynamicErrorMessage?.isEmpty ?? true
|
||||
updateUIDynamicError?()
|
||||
}
|
||||
}
|
||||
public var errorMessage: String?
|
||||
public var errorTextColor: Color?
|
||||
public var enabled: Bool = true
|
||||
public var required: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var showError: Bool?
|
||||
public var hideBorders = false
|
||||
public var locked: Bool?
|
||||
public var selected: Bool?
|
||||
public var text: String?
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var baseValue: AnyHashable?
|
||||
public var wasInitiallySelected: Bool = false
|
||||
public var text: String?
|
||||
public var title: String?
|
||||
public var feedback: String?
|
||||
public var shouldMaskRecordedView: Bool? = true
|
||||
|
||||
//used to drive the EntryFieldView UI
|
||||
public var titleStateLabel: FormLabelModel
|
||||
public var feedbackStateLabel: FormLabelModel
|
||||
|
||||
public var isValid: Bool? = true {
|
||||
didSet { updateUI?() }
|
||||
}
|
||||
|
||||
/// Temporary binding mechanism for the view to update on enable changes.
|
||||
public var updateUI: ActionBlock?
|
||||
|
||||
// TODO: Remove once updateUI is fixed with isSelected
|
||||
public var updateUIDynamicError: ActionBlock?
|
||||
public var titleStateLabel = FormLabelModel(text: "")
|
||||
public var feedbackStateLabel = FormLabelModel(text: "")
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case title
|
||||
case enabled
|
||||
case readOnly
|
||||
case feedback
|
||||
case errorMessage
|
||||
case errorTextColor
|
||||
case locked
|
||||
case selected
|
||||
case showError
|
||||
case hideBorders
|
||||
case text
|
||||
case fieldKey
|
||||
case groupName
|
||||
case required
|
||||
case shouldMaskRecordedView
|
||||
}
|
||||
|
||||
@ -92,7 +52,7 @@ import Foundation
|
||||
// MARK: - Validation Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
open override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
|
||||
if dynamicErrorMessage != nil {
|
||||
@ -100,30 +60,15 @@ import Foundation
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
}
|
||||
|
||||
public func setValidity(_ valid: Bool, errorMessage: String?) {
|
||||
|
||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||
self.errorMessage = ruleErrorMessage
|
||||
}
|
||||
|
||||
self.isValid = valid
|
||||
updateUI?()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(with text: String) {
|
||||
super.init()
|
||||
self.text = text
|
||||
baseValue = text
|
||||
self.titleStateLabel = FormLabelModel(text: "")
|
||||
self.feedbackStateLabel = FormLabelModel(text: "")
|
||||
setDefaults()
|
||||
}
|
||||
|
||||
@ -131,7 +76,7 @@ import Foundation
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public func clear() {
|
||||
self.text = ""
|
||||
text = ""
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -139,54 +84,35 @@ import Foundation
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
try super.init(from: decoder)
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
title = try typeContainer.decodeIfPresent(String.self, forKey: .title)
|
||||
feedback = try typeContainer.decodeIfPresent(String.self, forKey: .feedback)
|
||||
errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||
errorTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .errorTextColor)
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
locked = try typeContainer.decodeIfPresent(Bool.self, forKey: .locked)
|
||||
selected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected)
|
||||
text = try typeContainer.decodeIfPresent(String.self, forKey: .text)
|
||||
hideBorders = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideBorders) ?? false
|
||||
baseValue = text
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
shouldMaskRecordedView = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskRecordedView) ?? shouldMaskRecordedView
|
||||
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
|
||||
self.groupName = groupName
|
||||
}
|
||||
self.titleStateLabel = FormLabelModel(text: title ?? "")
|
||||
self.feedbackStateLabel = FormLabelModel(model: LabelModel(text: feedback ?? "",
|
||||
titleStateLabel = FormLabelModel(text: title ?? "")
|
||||
feedbackStateLabel = FormLabelModel(model: LabelModel(text: feedback ?? "",
|
||||
fontStyle: FormLabelModel.defaultFontStyle,
|
||||
textColor: Color(uiColor: .mvmCoolGray6)))
|
||||
setDefaults()
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
open override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
try container.encodeIfPresent(feedback, forKey: .feedback)
|
||||
try container.encodeIfPresent(text, forKey: .text)
|
||||
try container.encodeIfPresent(locked, forKey: .locked)
|
||||
try container.encodeIfPresent(showError, forKey: .showError)
|
||||
try container.encodeIfPresent(selected, forKey: .selected)
|
||||
try container.encodeIfPresent(errorTextColor, forKey: .errorTextColor)
|
||||
try container.encodeIfPresent(errorMessage, forKey: .errorMessage)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(required, forKey: .required)
|
||||
try container.encode(hideBorders, forKey: .hideBorders)
|
||||
try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView)
|
||||
}
|
||||
|
||||
@ -96,3 +96,17 @@ open class TooltipModel: MoleculeModelProtocol {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TooltipModel {
|
||||
public func toVDSTooltipModel() -> Tooltip.TooltipModel {
|
||||
var moleculeView: MoleculeViewProtocol?
|
||||
if let molecule, let view = ModelRegistry.createMolecule(molecule) {
|
||||
moleculeView = view
|
||||
}
|
||||
return .init(closeButtonText: closeButtonText,
|
||||
title: title,
|
||||
content: content,
|
||||
contentView: moleculeView
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,21 +25,31 @@ extension VDS.ButtonIcon.Size: Codable {}
|
||||
extension VDS.ButtonIcon.BadgeIndicatorModel.ExpandDirection: Codable {}
|
||||
extension VDS.ButtonIcon.SurfaceType: Codable {}
|
||||
extension VDS.ButtonGroup.Alignment: Codable {}
|
||||
extension VDS.CarouselScrollbar.Layout: Codable {}
|
||||
extension VDS.DatePicker.DateFormat: Codable {}
|
||||
extension VDS.EntryFieldBase.HelperTextPlacement: Codable {}
|
||||
extension VDS.Icon.Name: Codable {}
|
||||
extension VDS.Icon.Size: Codable {}
|
||||
extension VDS.InputField.CreditCardType: Codable {}
|
||||
extension VDS.InputField.DateFormat: Codable {}
|
||||
extension VDS.InputField.FieldType: Codable {}
|
||||
extension VDS.Line.Style: Codable {}
|
||||
extension VDS.Line.Orientation: Codable {}
|
||||
extension VDS.Tabs.Orientation: Codable {}
|
||||
extension VDS.Tabs.IndicatorPosition: Codable {}
|
||||
extension VDS.Tabs.Overflow: Codable {}
|
||||
extension VDS.Tabs.Size: Codable {}
|
||||
extension VDS.TextArea.Height: Codable {}
|
||||
extension VDS.TextLink.Size: Codable {}
|
||||
extension VDS.TextLinkCaret.IconPosition: Codable {}
|
||||
extension VDS.TileContainerBase.AspectRatio: Codable {}
|
||||
extension VDS.Tilelet.Padding: Codable {}
|
||||
extension VDS.TitleLockup.TextAlignment: Codable {}
|
||||
extension VDS.Toggle.TextSize: Codable {}
|
||||
extension VDS.Toggle.TextPosition: Codable {}
|
||||
extension VDS.Toggle.TextWeight: Codable {}
|
||||
extension VDS.Tooltip.FillColor: Codable {}
|
||||
extension VDS.Tooltip.Size: Codable {}
|
||||
extension VDS.Line.Style: Codable {}
|
||||
extension VDS.Line.Orientation: Codable {}
|
||||
extension VDS.Use: Codable {}
|
||||
|
||||
extension VDS.Button.Size: RawRepresentableCodable {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
// Form fields are items can be interacted with. They have value, and may need to be validated.
|
||||
|
||||
import VDS
|
||||
|
||||
public protocol FormFieldProtocol: FormItemProtocol {
|
||||
|
||||
@ -36,6 +36,21 @@ public extension FormFieldProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol FormFieldInternalValidatableProtocol: FormFieldProtocol {
|
||||
associatedtype ValueType = AnyHashable
|
||||
var rules: [AnyRule<ValueType>]? { get set }
|
||||
var internalRules: [RuleAnyModelProtocol]? { get }
|
||||
}
|
||||
|
||||
extension FormFieldInternalValidatableProtocol {
|
||||
public var internalRules: [RuleAnyModelProtocol]? {
|
||||
guard let fieldKey else { return nil }
|
||||
return rules?.compactMap{ rule in
|
||||
return RuleVDSModel(field: fieldKey, rule: rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FormFieldValidity{
|
||||
public var fieldKey: String
|
||||
public var valid: Bool = true
|
||||
|
||||
@ -44,6 +44,29 @@ import MVMCore
|
||||
if let fieldKey = field.fieldKey {
|
||||
fields[fieldKey] = field
|
||||
}
|
||||
// add internal validators if needed
|
||||
if let field = field as? any FormFieldInternalValidatableProtocol {
|
||||
addInternalRules(field)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds additional Rules that are from another source
|
||||
private func addInternalRules(_ field: any FormFieldInternalValidatableProtocol) {
|
||||
if let internalRules = field.internalRules, !internalRules.isEmpty {
|
||||
|
||||
//find the group
|
||||
if let formGroup = formRules?.first(where: {$0.groupName == field.groupName}) {
|
||||
formGroup.rules.append(contentsOf: internalRules)
|
||||
} else {
|
||||
//create the new group
|
||||
let formGroup = FormGroupRule(field.groupName, internalRules, [])
|
||||
if var formRules {
|
||||
formRules.append(formGroup)
|
||||
} else {
|
||||
formRules = [formGroup]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the form action to the validator.
|
||||
@ -72,7 +95,6 @@ import MVMCore
|
||||
if let validator = delegate?.formValidator {
|
||||
validator.delegate = delegate
|
||||
validator.insert(item)
|
||||
|
||||
// TODO: Temporary hacks, rewrite architecture to support this.
|
||||
_ = validator.validate()
|
||||
}
|
||||
|
||||
59
MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift
Normal file
59
MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift
Normal file
@ -0,0 +1,59 @@
|
||||
//
|
||||
// RuleVDSModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 7/12/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class VDSRuleBase: RuleAnyModelProtocol {
|
||||
open var ruleId: String?
|
||||
open var errorMessage: [String : String]?
|
||||
open var fields = [String]()
|
||||
public init(){}
|
||||
|
||||
open func isValid(_ formField: any FormFieldProtocol) -> Bool {
|
||||
fatalError()
|
||||
}
|
||||
public static var identifier: String = "AnyVDSRule"
|
||||
}
|
||||
|
||||
public class RuleVDSModel<ValueType>: VDSRuleBase {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public var rule: AnyRule<ValueType>
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(field: String, rule: AnyRule<ValueType>) {
|
||||
self.rule = rule
|
||||
super.init()
|
||||
self.fields = [field]
|
||||
self.ruleId = "\(rule.self)-\(Int.random(in: 1...1000))"
|
||||
}
|
||||
|
||||
required init(from decoder: any Decoder) throws {
|
||||
fatalError("init(from:) has not been implemented")
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func isValid(_ formField: FormFieldProtocol) -> Bool {
|
||||
let value = formField.formFieldValue() as? ValueType
|
||||
let valid = rule.isValid(value: value)
|
||||
if let field = fields.first, !valid {
|
||||
errorMessage = [field: rule.errorMessage]
|
||||
} else {
|
||||
errorMessage = nil
|
||||
}
|
||||
return valid
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user