Skip to content

Commit 5467a22

Browse files
committed
Update Example, CompoundValidator and unit tests
1 parent 98ba419 commit 5467a22

File tree

6 files changed

+246
-76
lines changed

6 files changed

+246
-76
lines changed

Example/FormHookExample/ContentView.swift

Lines changed: 217 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,27 @@ import Hooks
1010
import FormHook
1111

1212
enum FormFieldName: String, CaseIterable {
13-
case username
14-
case password
15-
16-
var title: String {
17-
switch self {
18-
case .username:
19-
return "Username"
20-
case .password:
21-
return "Password"
22-
}
23-
}
13+
case firstName = "First name"
14+
case lastName = "Last name"
15+
case password = "Password"
16+
case gender = "Gender"
17+
case email = "Email"
18+
case phone = "Phone"
19+
case dob = "Date of birth"
2420

2521
func messages(for validationResult: Bool) -> [String] {
2622
if validationResult {
2723
return []
2824
}
29-
switch self {
30-
case .username:
31-
return ["Username is required"]
32-
case .password:
33-
return ["Password is required"]
34-
}
25+
return ["\(rawValue) is required"]
3526
}
3627
}
3728

29+
enum Gender: String, CaseIterable {
30+
case male = "Male"
31+
case female = "Female"
32+
}
33+
3834
struct ContentView: HookView {
3935

4036
@FocusState var focusField: FormFieldName?
@@ -43,49 +39,216 @@ struct ContentView: HookView {
4339
var hookBody: some View {
4440
ContextualForm { (form: FormControl<FormFieldName>) in
4541
Form {
46-
VStack(spacing: 16) {
47-
ForEach(FormFieldName.allCases, id: \.self) { name in
48-
Controller(
49-
name: name,
50-
defaultValue: "",
51-
rules: NotEmptyValidator(name.messages(for:))
52-
) { field, fieldState, _ in
53-
switch name {
54-
case .username:
55-
TextField(name.title, text: field.value)
56-
.focused($focusField, equals: name)
57-
.textContentType(.username)
58-
.submitLabel(.next)
59-
case .password:
60-
SecureField(name.title, text: field.value)
61-
.focused($focusField, equals: name)
62-
.textContentType(.password)
63-
.submitLabel(.go)
64-
}
42+
Section("Name") {
43+
firstNameView
44+
lastNameView
45+
}
46+
47+
Section("Password") {
48+
passwordView
49+
}
50+
51+
Section("Basic Info") {
52+
genderView
53+
dobView
54+
emailView
55+
phoneView
56+
}
57+
58+
Button("Submit") {
59+
focusField = nil
60+
hideKeyboard()
61+
Task {
62+
try await form.handleSubmit(onValid: { _, _ in
6563

66-
if let error = fieldState.error.first {
67-
Text(error)
68-
}
69-
}
70-
}
71-
72-
Button("Submit") {
73-
self.focusField = nil
74-
hideKeyboard()
75-
Task {
76-
do {
77-
try await form.handleSubmit(onValid: { _, _ in
78-
79-
}, onInvalid: { _, errors in
80-
self.focusField = FormFieldName.allCases.first(where: errors.errorFields.contains(_:))
81-
})
82-
} catch {}
83-
}
64+
}, onInvalid: { _, errors in
65+
focusField = FormFieldName.allCases.first(where: errors.errorFields.contains(_:))
66+
})
8467
}
8568
}
8669
}
8770
}
8871
}
72+
73+
var firstNameView: some View {
74+
Controller(
75+
name: FormFieldName.firstName,
76+
defaultValue: "",
77+
rules: NotEmptyValidator(FormFieldName.firstName.messages(for:))
78+
) { field, fieldState, _ in
79+
let textField = TextField(field.name.rawValue, text: field.value)
80+
.focused($focusField, equals: field.name)
81+
.submitLabel(.next)
82+
83+
if let error = fieldState.error.first {
84+
VStack {
85+
textField
86+
Text(error)
87+
.font(.system(size: 10)).foregroundColor(.red)
88+
}
89+
} else {
90+
textField
91+
}
92+
}
93+
}
94+
95+
var lastNameView: some View {
96+
Controller(
97+
name: FormFieldName.lastName,
98+
defaultValue: "",
99+
rules: NotEmptyValidator(FormFieldName.lastName.messages(for:))
100+
) { field, fieldState, _ in
101+
let textField = TextField(field.name.rawValue, text: field.value)
102+
.focused($focusField, equals: field.name)
103+
.submitLabel(.next)
104+
105+
if let error = fieldState.error.first {
106+
VStack {
107+
textField
108+
Text(error)
109+
.font(.system(size: 10)).foregroundColor(.red)
110+
}
111+
} else {
112+
textField
113+
}
114+
}
115+
}
116+
117+
var passwordView: some View {
118+
Controller(
119+
name: FormFieldName.password,
120+
defaultValue: "",
121+
rules: NotEmptyValidator(FormFieldName.password.messages(for:))
122+
) { field, fieldState, _ in
123+
let textField = SecureField(field.name.rawValue, text: field.value)
124+
.focused($focusField, equals: field.name)
125+
.textContentType(.newPassword)
126+
.submitLabel(.go)
127+
128+
if let error = fieldState.error.first {
129+
VStack {
130+
textField
131+
Text(error)
132+
.font(.system(size: 10)).foregroundColor(.red)
133+
}
134+
} else {
135+
textField
136+
}
137+
}
138+
}
139+
140+
var genderView: some View {
141+
Controller(
142+
name: FormFieldName.gender,
143+
defaultValue: Gender.male
144+
) { field, fieldState, _ in
145+
let picker = Picker(field.name.rawValue, selection: field.value) {
146+
ForEach(Gender.allCases, id: \.self) { gender in
147+
Text(gender.rawValue)
148+
}
149+
}
150+
151+
if let error = fieldState.error.first {
152+
VStack {
153+
picker
154+
Text(error)
155+
.font(.system(size: 10)).foregroundColor(.red)
156+
.font(.system(size: 10)).foregroundColor(.red)
157+
}
158+
} else {
159+
picker
160+
}
161+
}
162+
}
163+
164+
var dobView: some View {
165+
Controller(
166+
name: FormFieldName.dob,
167+
defaultValue: Date()
168+
) { field, fieldState, _ in
169+
let picker = DatePicker(
170+
selection: field.value,
171+
in: ...Date.now,
172+
displayedComponents: .date
173+
) {
174+
Text(field.name.rawValue)
175+
}
176+
177+
if let error = fieldState.error.first {
178+
VStack {
179+
picker
180+
Text(error)
181+
.font(.system(size: 10)).foregroundColor(.red)
182+
}
183+
} else {
184+
picker
185+
}
186+
}
187+
}
188+
189+
@ViewBuilder
190+
var phoneView: some View {
191+
let phoneRegEx = "^\\d{3}-\\d{3}-\\d{4}$"
192+
let phonePatternValidator = PatternMatchingValidator<String>(pattern: phoneRegEx) { result in
193+
if result {
194+
return []
195+
}
196+
return ["Phone is not correct"]
197+
}
198+
199+
Controller(
200+
name: FormFieldName.phone,
201+
defaultValue: "",
202+
rules: NotEmptyValidator(FormFieldName.phone.messages(for:)).and(validator: phonePatternValidator)
203+
) { field, fieldState, _ in
204+
let textField = TextField(field.name.rawValue, text: field.value)
205+
.focused($focusField, equals: field.name)
206+
.keyboardType(.numberPad)
207+
.submitLabel(.next)
208+
209+
if let error = fieldState.error.first {
210+
VStack {
211+
textField
212+
Text(error)
213+
.font(.system(size: 10)).foregroundColor(.red)
214+
}
215+
} else {
216+
textField
217+
}
218+
}
219+
}
220+
221+
@ViewBuilder
222+
var emailView: some View {
223+
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
224+
let emailPatternValidator = PatternMatchingValidator<String>(pattern: emailRegEx) { result in
225+
if result {
226+
return []
227+
}
228+
return ["Email is not correct"]
229+
}
230+
231+
Controller(
232+
name: FormFieldName.email,
233+
defaultValue: "",
234+
rules: emailPatternValidator
235+
) { field, fieldState, _ in
236+
let textField = TextField(field.name.rawValue, text: field.value)
237+
.focused($focusField, equals: field.name)
238+
.keyboardType(.emailAddress)
239+
.submitLabel(.next)
240+
241+
if let error = fieldState.error.first {
242+
VStack {
243+
textField
244+
Text(error)
245+
.font(.system(size: 10)).foregroundColor(.red)
246+
}
247+
} else {
248+
textField
249+
}
250+
}
251+
}
89252
}
90253

91254
extension View {

Sources/FormHook/Form.swift

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,9 @@ public class FormControl<FieldName> where FieldName: Hashable {
6666
names.forEach { instantFormState.defaultValues[$0] = nil }
6767
}
6868
if options.contains(.keepIsValid) {
69-
return await syncFormState()
69+
return await updateValid()
7070
}
7171
names.forEach { instantFormState.errors.removeValidityOnly(name: $0) }
72-
await updateValid()
7372
}
7473

7574
public func unregister(name: FieldName..., options: UnregisterOption = []) async {
@@ -233,11 +232,10 @@ public class FormControl<FieldName> where FieldName: Hashable {
233232
if !options.contains(.keepSubmitCount) {
234233
instantFormState.submitCount = 0
235234
}
236-
237-
if options.contains(.keepErrors) {
238-
await syncFormState()
235+
if !options.contains(.keepErrors) {
236+
await updateValid()
239237
}
240-
return await updateValid()
238+
return await syncFormState()
241239
}
242240

243241
public func reset(name: FieldName, defaultValue: Any, options: SingleResetOption = []) async {
@@ -254,11 +252,11 @@ public class FormControl<FieldName> where FieldName: Hashable {
254252
if !options.contains(.keepDirty) {
255253
instantFormState.dirtyFields.remove(name)
256254
}
257-
if options.contains(.keepError) {
258-
return await syncFormState()
255+
if !options.contains(.keepError) {
256+
instantFormState.errors.remove(name: name)
257+
await updateValid()
259258
}
260-
instantFormState.errors.remove(name: name)
261-
await updateValid()
259+
return await syncFormState()
262260
}
263261

264262
public func clearErrors(names: [FieldName]) async {
@@ -365,11 +363,11 @@ public class FormControl<FieldName> where FieldName: Hashable {
365363
extension FormControl {
366364
func updateValid() async {
367365
guard instantFormState.isValid else {
368-
return await syncFormState()
366+
return
369367
}
370368
if let resolver = options.resolver {
371369
let isValid: Bool
372-
let result = await resolver(instantFormState.formValues, options.context, Array(formState.defaultValues.keys))
370+
let result = await resolver(instantFormState.formValues, options.context, Array(fields.keys))
373371
switch result {
374372
case .success(let formValues):
375373
isValid = true
@@ -457,6 +455,10 @@ private extension FormControl {
457455
options.shouldUnregister
458456
}
459457

458+
func computeResult() async -> Bool {
459+
await options.rules.isValid(value.wrappedValue)
460+
}
461+
460462
func computeMessages() async -> (Bool, [String]) {
461463
await options.rules.computeMessage(value: value.wrappedValue)
462464
}

Sources/FormHook/Validation/CompoundValidator.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@
88
import Foundation
99

1010
extension Validator {
11-
public func and<V>(shouldGetAllMessages: Bool = false, validator: V...) -> some Validator where V: Validator, V.Value == Value {
11+
public func and<V>(shouldGetAllMessages: Bool = false, validator: V...) -> some Validator<Value> where V: Validator, V.Value == Value {
1212
CompoundValidator<Value>(operator: .and, shouldGetAllMessages: shouldGetAllMessages, validator: [eraseToAnyValidator()] + validator.map { $0.eraseToAnyValidator() })
1313
}
1414

1515
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
16-
public func and(shouldGetAllMessages: Bool = false, validator: any Validator<Value>...) -> some Validator {
16+
public func and(shouldGetAllMessages: Bool = false, validator: any Validator<Value>...) -> some Validator<Value> {
1717
CompoundValidator<Value>(operator: .and, shouldGetAllMessages: shouldGetAllMessages, validator: ([self] + validator).map { $0.eraseToAnyValidator() })
1818
}
1919

20-
public func or<V>(shouldGetAllMessages: Bool = false, validator: V...) -> some Validator where V: Validator, V.Value == Value {
20+
public func or<V>(shouldGetAllMessages: Bool = false, validator: V...) -> some Validator<Value> where V: Validator, V.Value == Value {
2121
CompoundValidator<Value>(operator: .or, shouldGetAllMessages: shouldGetAllMessages, validator: [eraseToAnyValidator()] + validator.map { $0.eraseToAnyValidator() })
2222
}
2323

2424
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
25-
public func or(shouldGetAllMessages: Bool = false, validator: any Validator<Value>...) -> some Validator {
25+
public func or(shouldGetAllMessages: Bool = false, validator: any Validator<Value>...) -> some Validator<Value> {
2626
CompoundValidator<Value>(operator: .or, shouldGetAllMessages: shouldGetAllMessages, validator: ([self] + validator).map { $0.eraseToAnyValidator() })
2727
}
2828

29-
public func preMap<Input>(_ handler: @escaping (Input) async -> Value) -> some Validator {
29+
public func preMap<Input>(_ handler: @escaping (Input) async -> Value) -> some Validator<Input> {
3030
PreMapValidator<Input, Result, Value>(mapHandler: handler, validator: self)
3131
}
3232
}

0 commit comments

Comments
 (0)