Skip to content

Commit 01c0fb8

Browse files
committed
Support focused fields
1 parent b88df0c commit 01c0fb8

File tree

6 files changed

+192
-46
lines changed

6 files changed

+192
-46
lines changed

Example/FormHookExample/ContentView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ struct ContentView: HookView {
3737

3838
@ViewBuilder
3939
var hookBody: some View {
40-
ContextualForm { (form: FormControl<FormFieldName>) in
40+
ContextualForm(focusedFieldBinder: $focusField) { form in
4141
Form {
4242
Section("Name") {
4343
firstNameView
@@ -62,7 +62,7 @@ struct ContentView: HookView {
6262
try await form.handleSubmit(onValid: { _, _ in
6363

6464
}, onInvalid: { _, errors in
65-
focusField = FormFieldName.allCases.first(where: errors.errorFields.contains(_:))
65+
6666
})
6767
}
6868
}
@@ -122,7 +122,7 @@ struct ContentView: HookView {
122122
) { field, fieldState, _ in
123123
let textField = SecureField(field.name.rawValue, text: field.value)
124124
.focused($focusField, equals: field.name)
125-
.textContentType(.newPassword)
125+
.textContentType(.password)
126126
.submitLabel(.go)
127127

128128
if let error = fieldState.error.first {

Sources/FormHook/Form.swift

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
3535
}
3636
field.options = options
3737
} else {
38-
field = Field(name: name, options: options, control: self)
38+
field = Field(index: fields.count, name: name, options: options, control: self)
3939
fields[name] = field
4040
instantFormState.formValues[name] = options.defaultValue
4141
}
@@ -176,6 +176,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
176176
try await onValid(instantFormState.formValues, errors)
177177
} else if let onInvalid {
178178
try await onInvalid(instantFormState.formValues, errors)
179+
await focusError(with: errors)
179180
}
180181
await postHandleSubmit(isOveralValid: isOveralValid, errors: errors, isSubmitSuccessful: errors.errorFields.isEmpty)
181182
} catch {
@@ -184,6 +185,18 @@ public class FormControl<FieldName> where FieldName: Hashable {
184185
}
185186
}
186187

188+
private func focusError(with errors: FormError<FieldName>) async {
189+
guard options.shouldFocusError else {
190+
return
191+
}
192+
let fields = fields
193+
.sorted { $0.value.index < $1.value.index }
194+
guard let firstErrorField = fields.first(where: { errors.errorFields.contains($0.key) })?.key else {
195+
return
196+
}
197+
await options.focusedFieldOption.triggerFocus(on: firstErrorField)
198+
}
199+
187200
private func postHandleSubmit(isOveralValid: Bool, errors: FormError<FieldName>, isSubmitSuccessful: Bool) async {
188201
instantFormState.isValid = isOveralValid
189202
instantFormState.submissionState = .submitted
@@ -450,6 +463,7 @@ extension FormControl {
450463

451464
private extension FormControl {
452465
class Field<Value>: FieldProtocol {
466+
let index: Int
453467
let name: FieldName
454468
var options: RegisterOption<Value> {
455469
didSet {
@@ -462,7 +476,8 @@ private extension FormControl {
462476
unowned var control: FormControl<FieldName>
463477
var value: Binding<Value>
464478

465-
init(name: FieldName, options: RegisterOption<Value>, control: FormControl<FieldName>) {
479+
init(index: Int, name: FieldName, options: RegisterOption<Value>, control: FormControl<FieldName>) {
480+
self.index = index
466481
self.name = name
467482
self.options = options
468483
self.control = control
@@ -498,6 +513,7 @@ private extension FormControl {
498513
}
499514
Task {
500515
await self.trigger(name: name)
516+
await self.options.focusedFieldOption.triggerFocus(on: name)
501517
}
502518
}
503519
}
@@ -522,41 +538,59 @@ private extension FormControl {
522538
}
523539

524540
public struct ContextualForm<Content, FieldName>: View where Content: View, FieldName: Hashable {
525-
let mode: Mode
526-
let reValidateMode: ReValidateMode
527-
let resolver: Resolver<FieldName>?
528-
let context: Any?
529-
let shouldUnregister: Bool
530-
let delayErrorInNanoseconds: UInt64
541+
let formOptions: FormOption<FieldName>
531542
let contentBuilder: (FormControl<FieldName>) -> Content
532543

533544
public init(mode: Mode = .onSubmit,
534545
reValidateMode: ReValidateMode = .onChange,
535546
resolver: Resolver<FieldName>? = nil,
536547
context: Any? = nil,
537548
shouldUnregister: Bool = true,
549+
shouldFocusError: Bool = true,
550+
delayErrorInNanoseconds: UInt64 = 0,
551+
onFocusedField: @escaping (FieldName) -> Void,
552+
@ViewBuilder content: @escaping (FormControl<FieldName>) -> Content
553+
) {
554+
self.formOptions = .init(
555+
mode: mode,
556+
reValidateMode: reValidateMode,
557+
resolver: resolver,
558+
context: context,
559+
shouldUnregister: shouldUnregister,
560+
shouldFocusError: shouldFocusError,
561+
delayErrorInNanoseconds: delayErrorInNanoseconds,
562+
onFocusedField: onFocusedField
563+
)
564+
self.contentBuilder = content
565+
}
566+
567+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
568+
public init(mode: Mode = .onSubmit,
569+
reValidateMode: ReValidateMode = .onChange,
570+
resolver: Resolver<FieldName>? = nil,
571+
context: Any? = nil,
572+
shouldUnregister: Bool = true,
573+
shouldFocusError: Bool = true,
538574
delayErrorInNanoseconds: UInt64 = 0,
575+
focusedFieldBinder: FocusState<FieldName?>.Binding,
539576
@ViewBuilder content: @escaping (FormControl<FieldName>) -> Content
540577
) {
541-
self.mode = mode
542-
self.reValidateMode = reValidateMode
543-
self.resolver = resolver
544-
self.context = context
545-
self.shouldUnregister = shouldUnregister
546-
self.delayErrorInNanoseconds = delayErrorInNanoseconds
578+
self.formOptions = .init(
579+
mode: mode,
580+
reValidateMode: reValidateMode,
581+
resolver: resolver,
582+
context: context,
583+
shouldUnregister: shouldUnregister,
584+
shouldFocusError: shouldFocusError,
585+
delayErrorInNanoseconds: delayErrorInNanoseconds,
586+
focusedStateBinder: focusedFieldBinder
587+
)
547588
self.contentBuilder = content
548589
}
549590

550591
public var body: some View {
551592
HookScope {
552-
let form = useForm(
553-
mode: mode,
554-
reValidateMode: reValidateMode,
555-
resolver: resolver,
556-
context: context,
557-
shouldUnregister: shouldUnregister,
558-
delayErrorInNanoseconds: delayErrorInNanoseconds
559-
)
593+
let form = useForm(formOptions)
560594
Context.Provider(value: form) {
561595
contentBuilder(form)
562596
}

Sources/FormHook/Hook/UseForm.swift

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77

88
import Foundation
99
import Hooks
10+
import SwiftUI
1011

1112
public func useForm<FieldName>(
1213
mode: Mode = .onSubmit,
1314
reValidateMode: ReValidateMode = .onChange,
1415
resolver: Resolver<FieldName>? = nil,
1516
context: Any? = nil,
1617
shouldUnregister: Bool = true,
17-
delayErrorInNanoseconds: UInt64 = 0
18+
shouldFocusError: Bool,
19+
delayErrorInNanoseconds: UInt64 = 0,
20+
onFocusedField: @escaping (FieldName) -> Void
1821
) -> FormControl<FieldName> where FieldName: Hashable {
1922
useForm(
2023
FormOption(
@@ -23,7 +26,34 @@ public func useForm<FieldName>(
2326
resolver: resolver,
2427
context: context,
2528
shouldUnregister: shouldUnregister,
26-
delayErrorInNanoseconds: delayErrorInNanoseconds
29+
shouldFocusError: shouldFocusError,
30+
delayErrorInNanoseconds: delayErrorInNanoseconds,
31+
onFocusedField: onFocusedField
32+
)
33+
)
34+
}
35+
36+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
37+
public func useForm<FieldName>(
38+
mode: Mode = .onSubmit,
39+
reValidateMode: ReValidateMode = .onChange,
40+
resolver: Resolver<FieldName>? = nil,
41+
context: Any? = nil,
42+
shouldUnregister: Bool = true,
43+
shouldFocusError: Bool = true,
44+
delayErrorInNanoseconds: UInt64 = 0,
45+
focusedStateBinder: FocusState<FieldName?>.Binding
46+
) -> FormControl<FieldName> where FieldName: Hashable {
47+
useForm(
48+
FormOption(
49+
mode: mode,
50+
reValidateMode: reValidateMode,
51+
resolver: resolver,
52+
context: context,
53+
shouldUnregister: shouldUnregister,
54+
shouldFocusError: shouldFocusError,
55+
delayErrorInNanoseconds: delayErrorInNanoseconds,
56+
focusedStateBinder: focusedStateBinder
2757
)
2858
)
2959
}
@@ -41,21 +71,72 @@ public struct FormOption<FieldName> where FieldName: Hashable {
4171
var resolver: Resolver<FieldName>?
4272
var context: Any?
4373
var shouldUnregister: Bool
74+
var shouldFocusError: Bool
4475
var delayErrorInNanoseconds: UInt64
76+
let focusedFieldOption: FocusedFieldOption
4577

4678
init(mode: Mode,
4779
reValidateMode: ReValidateMode,
4880
@_implicitSelfCapture resolver: Resolver<FieldName>?,
4981
context: Any?,
5082
shouldUnregister: Bool,
51-
delayErrorInNanoseconds: UInt64
83+
shouldFocusError: Bool,
84+
delayErrorInNanoseconds: UInt64,
85+
onFocusedField: @escaping (FieldName) -> Void
5286
) {
5387
self.mode = mode
5488
self.reValidateMode = reValidateMode
5589
self.resolver = resolver
5690
self.context = context
5791
self.shouldUnregister = shouldUnregister
92+
self.shouldFocusError = shouldFocusError
5893
self.delayErrorInNanoseconds = delayErrorInNanoseconds
94+
self.focusedFieldOption = .init(onFocusedField)
95+
}
96+
97+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
98+
init(mode: Mode,
99+
reValidateMode: ReValidateMode,
100+
@_implicitSelfCapture resolver: Resolver<FieldName>?,
101+
context: Any?,
102+
shouldUnregister: Bool,
103+
shouldFocusError: Bool,
104+
delayErrorInNanoseconds: UInt64,
105+
focusedStateBinder: FocusState<FieldName?>.Binding
106+
) {
107+
self.mode = mode
108+
self.reValidateMode = reValidateMode
109+
self.resolver = resolver
110+
self.context = context
111+
self.shouldUnregister = shouldUnregister
112+
self.shouldFocusError = shouldFocusError
113+
self.delayErrorInNanoseconds = delayErrorInNanoseconds
114+
self.focusedFieldOption = .init(focusedStateBinder)
115+
}
116+
117+
struct FocusedFieldOption {
118+
let focusedFieldBinder: Any?
119+
let onFocusedField: ((FieldName) -> Void)?
120+
121+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
122+
init(_ focusedFieldBinder: FocusState<FieldName?>.Binding) {
123+
self.focusedFieldBinder = focusedFieldBinder
124+
self.onFocusedField = nil
125+
}
126+
127+
init(_ onFocusedField: @escaping (FieldName) -> Void) {
128+
self.focusedFieldBinder = nil
129+
self.onFocusedField = onFocusedField
130+
}
131+
132+
@MainActor
133+
func triggerFocus(on field: FieldName) {
134+
if #available(macOS 12.0, iOS 15.0, tvOS 15.0, *), let focusedFieldBinder = focusedFieldBinder {
135+
(focusedFieldBinder as? FocusState<FieldName?>.Binding)?.wrappedValue = field
136+
} else {
137+
onFocusedField?(field)
138+
}
139+
}
59140
}
60141
}
61142

Sources/FormHook/Types.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ public struct FieldState {
210210
}
211211

212212
protocol FieldProtocol {
213+
var index: Int { get }
213214
var shouldUnregister: Bool { get }
214215
func computeMessages() async -> (Bool, [String])
215216
}

0 commit comments

Comments
 (0)