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:
Pfeil, Scott Robert 2024-07-30 17:47:22 +00:00
commit fbecc82d20
10 changed files with 469 additions and 152 deletions

View File

@ -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 */,

View 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)
}
}

View File

@ -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]
}
}

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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

View File

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

View 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
}
}