Skip to content

Commit 9e30f19

Browse files
committed
Focus on error fields properly
1 parent bdc9ebd commit 9e30f19

File tree

6 files changed

+121
-53
lines changed

6 files changed

+121
-53
lines changed

Example/FormHookExample/ContentView.swift

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ struct ContentView: HookView {
7474
Controller(
7575
name: FormFieldName.firstName,
7676
defaultValue: "",
77-
rules: NotEmptyValidator(FormFieldName.firstName.messages(for:))
77+
rules: NotEmptyValidator(FormFieldName.firstName.messages(for:)),
78+
fieldOrdinal: 0
7879
) { field, fieldState, _ in
7980
let textField = TextField(field.name.rawValue, text: field.value)
8081
.focused($focusField, equals: field.name)
@@ -96,7 +97,8 @@ struct ContentView: HookView {
9697
Controller(
9798
name: FormFieldName.lastName,
9899
defaultValue: "",
99-
rules: NotEmptyValidator(FormFieldName.lastName.messages(for:))
100+
rules: NotEmptyValidator(FormFieldName.lastName.messages(for:)),
101+
fieldOrdinal: 1
100102
) { field, fieldState, _ in
101103
let textField = TextField(field.name.rawValue, text: field.value)
102104
.focused($focusField, equals: field.name)
@@ -118,7 +120,8 @@ struct ContentView: HookView {
118120
Controller(
119121
name: FormFieldName.password,
120122
defaultValue: "",
121-
rules: NotEmptyValidator(FormFieldName.password.messages(for:))
123+
rules: NotEmptyValidator(FormFieldName.password.messages(for:)),
124+
fieldOrdinal: 2
122125
) { field, fieldState, _ in
123126
let textField = SecureField(field.name.rawValue, text: field.value)
124127
.focused($focusField, equals: field.name)
@@ -140,7 +143,8 @@ struct ContentView: HookView {
140143
var genderView: some View {
141144
Controller(
142145
name: FormFieldName.gender,
143-
defaultValue: Gender.male
146+
defaultValue: Gender.male,
147+
fieldOrdinal: 3
144148
) { field, fieldState, _ in
145149
let picker = Picker(field.name.rawValue, selection: field.value) {
146150
ForEach(Gender.allCases, id: \.self) { gender in
@@ -163,7 +167,8 @@ struct ContentView: HookView {
163167
var dobView: some View {
164168
Controller(
165169
name: FormFieldName.dob,
166-
defaultValue: Calendar.current.startOfDay(for: Date())
170+
defaultValue: Calendar.current.startOfDay(for: Date()),
171+
fieldOrdinal: 4
167172
) { field, fieldState, _ in
168173
let picker = DatePicker(
169174
selection: field.value,
@@ -186,23 +191,25 @@ struct ContentView: HookView {
186191
}
187192

188193
@ViewBuilder
189-
var phoneView: some View {
190-
let phoneRegEx = "^\\d{3}-\\d{3}-\\d{4}$"
191-
let phonePatternValidator = PatternMatchingValidator<String>(pattern: phoneRegEx) { result in
194+
var emailView: some View {
195+
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
196+
let emailPatternValidator = PatternMatchingValidator<String>(pattern: emailRegEx) { result in
192197
if result {
193198
return []
194199
}
195-
return ["Phone is not correct"]
200+
return ["Email is not correct"]
196201
}
197202

198203
Controller(
199-
name: FormFieldName.phone,
204+
name: FormFieldName.email,
200205
defaultValue: "",
201-
rules: NotEmptyValidator(FormFieldName.phone.messages(for:)).and(validator: phonePatternValidator)
206+
rules: NotEmptyValidator(FormFieldName.email.messages(for:))
207+
.and(validator: emailPatternValidator),
208+
fieldOrdinal: 5
202209
) { field, fieldState, _ in
203210
let textField = TextField(field.name.rawValue, text: field.value)
204211
.focused($focusField, equals: field.name)
205-
.keyboardType(.numberPad)
212+
.keyboardType(.emailAddress)
206213
.submitLabel(.next)
207214

208215
if let error = fieldState.error.first {
@@ -218,24 +225,24 @@ struct ContentView: HookView {
218225
}
219226

220227
@ViewBuilder
221-
var emailView: some View {
222-
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
223-
let emailPatternValidator = PatternMatchingValidator<String>(pattern: emailRegEx) { result in
228+
var phoneView: some View {
229+
let phoneRegEx = "^\\d{3}-\\d{3}-\\d{4}$"
230+
let phonePatternValidator = PatternMatchingValidator<String>(pattern: phoneRegEx) { result in
224231
if result {
225232
return []
226233
}
227-
return ["Email is not correct"]
234+
return ["Phone is not correct"]
228235
}
229236

230237
Controller(
231-
name: FormFieldName.email,
238+
name: FormFieldName.phone,
232239
defaultValue: "",
233-
rules: NotEmptyValidator(FormFieldName.email.messages(for:))
234-
.and(validator: emailPatternValidator)
240+
rules: NotEmptyValidator(FormFieldName.phone.messages(for:)).and(validator: phonePatternValidator),
241+
fieldOrdinal: 6
235242
) { field, fieldState, _ in
236243
let textField = TextField(field.name.rawValue, text: field.value)
237244
.focused($focusField, equals: field.name)
238-
.keyboardType(.emailAddress)
245+
.keyboardType(.numberPad)
239246
.submitLabel(.next)
240247

241248
if let error = fieldState.error.first {

Sources/FormHook/Controller.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public struct Controller<Content, FieldName, Value>: View where Content: View, F
2727
let rules: any Validator<Value>
2828
let shouldUnregister: Bool
2929
let unregisterOption: UnregisterOption
30+
let fieldOrdinal: Int?
3031
let render: (ControllerRenderOption<FieldName, Value>) -> Content
3132

3233
public init(
@@ -35,6 +36,7 @@ public struct Controller<Content, FieldName, Value>: View where Content: View, F
3536
rules: any Validator<Value> = NoopValidator(),
3637
shouldUnregister: Bool = true,
3738
unregisterOption: UnregisterOption = [],
39+
fieldOrdinal: Int? = nil,
3840
@ViewBuilder render: @escaping (ControllerRenderOption<FieldName, Value>) -> Content
3941
) {
4042
self.name = name
@@ -43,11 +45,12 @@ public struct Controller<Content, FieldName, Value>: View where Content: View, F
4345
self.render = render
4446
self.shouldUnregister = true
4547
self.unregisterOption = unregisterOption
48+
self.fieldOrdinal = fieldOrdinal
4649
}
4750

4851
public var body: some View {
4952
HookScope {
50-
let renderOption = useController(name: name, defaultValue: defaultValue, rules: rules, shouldUnregister: shouldUnregister, unregisterOption: unregisterOption)
53+
let renderOption = useController(name: name, defaultValue: defaultValue, rules: rules, shouldUnregister: shouldUnregister, unregisterOption: unregisterOption, fieldOrdinal: fieldOrdinal)
5154
render(renderOption)
5255
}
5356
}

Sources/FormHook/Form.swift

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ public class FormControl<FieldName> where FieldName: Hashable {
1818
private(set) public var formState: FormState<FieldName>
1919
var instantFormState: FormState<FieldName>
2020

21+
var _currentFocusedField: FieldName?
22+
var currentFocusedField: FieldName? {
23+
get {
24+
_currentFocusedField ?? options.focusedFieldOption.focusedFieldBindingValue
25+
}
26+
set {
27+
if options.focusedFieldOption.hasFocusedFieldBinder {
28+
return
29+
}
30+
_currentFocusedField = newValue
31+
}
32+
}
33+
2134
init(options: FormOption<FieldName>, formState: Binding<FormState<FieldName>>) {
2235
self.options = options
2336
self.fields = [:]
@@ -34,8 +47,11 @@ public class FormControl<FieldName> where FieldName: Hashable {
3447
instantFormState.formValues[name] = options.defaultValue
3548
}
3649
field.options = options
50+
if let fieldOrdinal = options.fieldOrdinal {
51+
field.fieldOrdinal = fieldOrdinal
52+
}
3753
} else {
38-
field = Field(index: fields.count, name: name, options: options, control: self)
54+
field = Field(fieldOrdinal: options.fieldOrdinal ?? fields.count, name: name, options: options, control: self)
3955
fields[name] = field
4056
instantFormState.formValues[name] = options.defaultValue
4157
}
@@ -98,7 +114,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
98114
case .success(let formValues):
99115
isOveralValid = true
100116
errors = .init()
101-
instantFormState.formValues.update(other: formValues)
117+
instantFormState.formValues.unioned(formValues)
102118
case .failure(let e):
103119
isOveralValid = false
104120
errors = e
@@ -138,7 +154,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
138154
case .success(let formValues):
139155
isOveralValid = true
140156
errors = .init()
141-
instantFormState.formValues.update(other: formValues)
157+
instantFormState.formValues.unioned(formValues)
142158
case .failure(let e):
143159
isOveralValid = false
144160
errors = e
@@ -176,25 +192,37 @@ public class FormControl<FieldName> where FieldName: Hashable {
176192
try await onValid(instantFormState.formValues, errors)
177193
} else if let onInvalid {
178194
try await onInvalid(instantFormState.formValues, errors)
179-
await focusError(with: errors)
180195
}
181196
await postHandleSubmit(isOveralValid: isOveralValid, errors: errors, isSubmitSuccessful: errors.errorFields.isEmpty)
197+
if !isOveralValid {
198+
await focusError(with: errors)
199+
}
182200
} catch {
183201
await postHandleSubmit(isOveralValid: isOveralValid, errors: errors, isSubmitSuccessful: false)
184202
throw error
185203
}
186204
}
187205

188-
private func focusError(with errors: FormError<FieldName>) async {
206+
@MainActor
207+
private func focusError(with errors: FormError<FieldName>) {
189208
guard options.shouldFocusError else {
190209
return
191210
}
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 {
211+
let fields = fields.sorted { $0.value.fieldOrdinal < $1.value.fieldOrdinal }
212+
let firstErrorField = fields.first(where: { errors.errorFields.contains($0.key) })?.key
213+
guard let firstErrorField else {
214+
return
215+
}
216+
currentFocusedField = firstErrorField
217+
options.focusedFieldOption.triggerFocus(on: firstErrorField)
218+
}
219+
220+
@MainActor
221+
private func focusOnCurrentField() {
222+
guard let currentFocusedField else {
195223
return
196224
}
197-
await options.focusedFieldOption.triggerFocus(on: firstErrorField)
225+
options.focusedFieldOption.triggerFocus(on: currentFocusedField)
198226
}
199227

200228
private func postHandleSubmit(isOveralValid: Bool, errors: FormError<FieldName>, isSubmitSuccessful: Bool) async {
@@ -214,6 +242,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
214242
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
215243
self?.instantFormState.errors = errors
216244
await self?.syncFormState()
245+
await self?.focusOnCurrentField()
217246
}
218247
}
219248
}
@@ -301,7 +330,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
301330
case .success:
302331
break
303332
case .failure(let e):
304-
instantFormState.errors = instantFormState.errors.rewrite(from: e)
333+
instantFormState.errors = instantFormState.errors.union(e)
305334
instantFormState.isValid = false
306335
}
307336
} else if let field = fields[name] {
@@ -335,10 +364,10 @@ public class FormControl<FieldName> where FieldName: Hashable {
335364
case .success(let formValues):
336365
isValid = true
337366
errors = instantFormState.errors
338-
instantFormState.formValues.update(other: formValues)
367+
instantFormState.formValues.unioned(formValues)
339368
case .failure(let e):
340369
isValid = false
341-
errors = instantFormState.errors.rewrite(from: e)
370+
errors = instantFormState.errors.union(e)
342371
}
343372
} else {
344373
(isValid, errors) = await withTaskGroup(of: KeyValidationResult.self) { group in
@@ -377,6 +406,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
377406
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
378407
self?.instantFormState.errors = errors
379408
await self?.syncFormState()
409+
await self?.focusOnCurrentField()
380410
}
381411
}
382412
return isValid
@@ -399,7 +429,7 @@ extension FormControl {
399429
switch result {
400430
case .success(let formValues):
401431
isValid = true
402-
instantFormState.formValues.update(other: formValues)
432+
instantFormState.formValues.unioned(formValues)
403433
case .failure(let e):
404434
isValid = false
405435
currentErrorNotifyTask?.cancel()
@@ -412,6 +442,7 @@ extension FormControl {
412442
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
413443
self?.instantFormState.errors = e
414444
await self?.syncFormState()
445+
await self?.focusOnCurrentField()
415446
}
416447
}
417448
}
@@ -445,6 +476,7 @@ extension FormControl {
445476
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
446477
self?.instantFormState.errors = errors
447478
await self?.syncFormState()
479+
await self?.focusOnCurrentField()
448480
}
449481
}
450482
instantFormState.isValid = isValid
@@ -463,7 +495,7 @@ extension FormControl {
463495

464496
private extension FormControl {
465497
class Field<Value>: FieldProtocol {
466-
let index: Int
498+
var fieldOrdinal: Int
467499
let name: FieldName
468500
var options: RegisterOption<Value> {
469501
didSet {
@@ -476,8 +508,8 @@ private extension FormControl {
476508
unowned var control: FormControl<FieldName>
477509
var value: Binding<Value>
478510

479-
init(index: Int, name: FieldName, options: RegisterOption<Value>, control: FormControl<FieldName>) {
480-
self.index = index
511+
init(fieldOrdinal: Int, name: FieldName, options: RegisterOption<Value>, control: FormControl<FieldName>) {
512+
self.fieldOrdinal = fieldOrdinal
481513
self.name = name
482514
self.options = options
483515
self.control = control
@@ -501,7 +533,7 @@ private extension FormControl {
501533
.init { [weak self] in
502534
self?.instantFormState.formValues[name] as? Value ?? defaultValue
503535
} set: { [weak self] value in
504-
guard let self = self else {
536+
guard let self else {
505537
return
506538
}
507539
self.instantFormState.formValues[name] = value
@@ -512,8 +544,16 @@ private extension FormControl {
512544
return
513545
}
514546
Task {
515-
await self.trigger(name: name)
516-
await self.options.focusedFieldOption.triggerFocus(on: name)
547+
guard await self.trigger(name: name) else {
548+
return
549+
}
550+
await MainActor.run {
551+
if self.currentFocusedField == name {
552+
return
553+
}
554+
self.currentFocusedField = name
555+
self.options.focusedFieldOption.triggerFocus(on: name)
556+
}
517557
}
518558
}
519559
}

Sources/FormHook/Hook/UseController.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ public func useController<FieldName, Value>(
1313
defaultValue: Value,
1414
rules: any Validator<Value>,
1515
shouldUnregister: Bool = true,
16-
unregisterOption: UnregisterOption = []
16+
unregisterOption: UnregisterOption = [],
17+
fieldOrdinal: Int? = nil
1718
) -> ControllerRenderOption<FieldName, Value> where FieldName: Hashable {
1819
let form = useContext(Context<FormControl<FieldName>>.self)
19-
let registration = form.register(name: name, options: RegisterOption(rules: rules, defaultValue: defaultValue, shouldUnregister: shouldUnregister))
20+
let registration = form.register(name: name, options: RegisterOption(fieldOrdinal: fieldOrdinal, rules: rules, defaultValue: defaultValue, shouldUnregister: shouldUnregister))
2021

2122
let preservedChangedArray = [
2223
AnyEquatable(name),

0 commit comments

Comments
 (0)