Skip to content

Commit 58e42b0

Browse files
committed
Test focusField function
1 parent 0d8d26c commit 58e42b0

File tree

4 files changed

+388
-333
lines changed

4 files changed

+388
-333
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ var hookBody: some View {
135135
// this code achieves the same
136136

137137
@ViewBuilder
138-
var hookBody: some View {
138+
var body: some View {
139139
ContextualForm(...) { form in
140140
let (field, fieldState, formState) = useController(name: FieldName.username, defaultValue: "")
141141
TextField("Username", text: field.value)

Sources/FormHook/Form.swift

Lines changed: 112 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,32 @@ import SwiftUI
1010
import Hooks
1111

1212
public class FormControl<FieldName> where FieldName: Hashable {
13-
var currentErrorNotifyTask: Task<Void, Error>?
14-
1513
var options: FormOption<FieldName>
16-
var fields: [FieldName: FieldProtocol]
14+
15+
private var currentErrorNotifyTask: Task<Void, Error>?
16+
private var fields: [FieldName: FieldProtocol]
17+
18+
private var _currentFocusedField: FieldName?
19+
20+
@MainActor
21+
private var currentFocusedField: FieldName? {
22+
get {
23+
_currentFocusedField ?? options.focusedFieldOption.focusedFieldBindingValue
24+
}
25+
set {
26+
if let newValue {
27+
options.focusedFieldOption.triggerFocus(on: newValue)
28+
}
29+
if options.focusedFieldOption.hasFocusedFieldBinder {
30+
return
31+
}
32+
_currentFocusedField = newValue
33+
}
34+
}
35+
36+
var instantFormState: FormState<FieldName>
1737
@MainActor @Binding
1838
private(set) public var formState: FormState<FieldName>
19-
var instantFormState: FormState<FieldName>
2039

2140
init(options: FormOption<FieldName>, formState: Binding<FormState<FieldName>>) {
2241
self.options = options
@@ -181,50 +200,12 @@ public class FormControl<FieldName> where FieldName: Hashable {
181200
try await onInvalid(instantFormState.formValues, errors)
182201
}
183202
await postHandleSubmit(isOveralValid: isOveralValid, errors: errors, isSubmitSuccessful: errors.errorFields.isEmpty)
184-
if !isOveralValid {
185-
await focusError(with: errors)
186-
}
187203
} catch {
188204
await postHandleSubmit(isOveralValid: isOveralValid, errors: errors, isSubmitSuccessful: false)
189205
throw error
190206
}
191207
}
192208

193-
@MainActor
194-
private func focusError(with errors: FormError<FieldName>) {
195-
guard options.shouldFocusError else {
196-
return
197-
}
198-
let fields = fields.sorted { $0.value.fieldOrdinal < $1.value.fieldOrdinal }
199-
let firstErrorField = fields.first(where: { errors.errorFields.contains($0.key) })?.key
200-
guard let firstErrorField else {
201-
return
202-
}
203-
options.focusedFieldOption.triggerFocus(on: firstErrorField)
204-
}
205-
206-
private func postHandleSubmit(isOveralValid: Bool, errors: FormError<FieldName>, isSubmitSuccessful: Bool) async {
207-
instantFormState.isValid = isOveralValid
208-
instantFormState.submissionState = .submitted
209-
instantFormState.isSubmitSuccessful = isSubmitSuccessful
210-
currentErrorNotifyTask?.cancel()
211-
instantFormState.submitCount += 1
212-
if options.delayErrorInNanoseconds == 0 || isOveralValid {
213-
currentErrorNotifyTask = nil
214-
instantFormState.errors = errors
215-
await syncFormState()
216-
} else {
217-
await syncFormState()
218-
let delayErrorInNanoseconds = options.delayErrorInNanoseconds
219-
currentErrorNotifyTask = Task { [weak self] in
220-
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
221-
self?.instantFormState.errors = errors
222-
await self?.syncFormState()
223-
await self?.focusError(with: errors)
224-
}
225-
}
226-
}
227-
228209
public func reset(defaultValues: FormValue<FieldName>, options: ResetOption = []) async {
229210
for (name, defaultValue) in defaultValues {
230211
if let defaultValue = Optional.some(defaultValue).flattened() {
@@ -302,23 +283,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
302283
guard options.contains(.shouldValidate) else {
303284
return await syncFormState()
304285
}
305-
if let resolver = self.options.resolver {
306-
let result = await resolver(instantFormState.formValues, self.options.context, [name])
307-
switch result {
308-
case .success:
309-
break
310-
case .failure(let e):
311-
instantFormState.errors = instantFormState.errors.union(e)
312-
instantFormState.isValid = false
313-
}
314-
} else if let field = fields[name] {
315-
let (isValid, messages) = await field.computeMessages()
316-
instantFormState.errors.setMessages(name: name, messages: messages, isValid: isValid)
317-
if !isValid {
318-
instantFormState.isValid = false
319-
}
320-
}
321-
return await syncFormState()
286+
await trigger(name: name)
322287
}
323288

324289
public func getFieldState(name: FieldName) -> FieldState {
@@ -330,7 +295,7 @@ public class FormControl<FieldName> where FieldName: Hashable {
330295
}
331296

332297
@discardableResult
333-
public func trigger(names: [FieldName]) async -> Bool {
298+
public func trigger(names: [FieldName], shouldFocus: Bool = false) async -> Bool {
334299
let validationNames = names.isEmpty ? fields.map { $0.key } : names
335300
instantFormState.isValidating = true
336301
await syncFormState()
@@ -370,31 +335,36 @@ public class FormControl<FieldName> where FieldName: Hashable {
370335
}
371336
}
372337
if !isValid {
373-
instantFormState.isValid = false
338+
instantFormState.isValid = isValid
374339
}
375340
instantFormState.isValidating = false
341+
376342
currentErrorNotifyTask?.cancel()
377-
if options.delayErrorInNanoseconds == 0 {
343+
if options.delayErrorInNanoseconds == 0 || isValid {
344+
currentErrorNotifyTask = nil
378345
instantFormState.errors = errors
379346
await syncFormState()
347+
if shouldFocus {
348+
await focusFieldAfterTrigger(names: validationNames, errorFields: errors.errorFields)
349+
}
380350
} else {
381351
await syncFormState()
382352
let delayErrorInNanoseconds = options.delayErrorInNanoseconds
383353
currentErrorNotifyTask = Task { [weak self] in
384354
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
385355
self?.instantFormState.errors = errors
386356
await self?.syncFormState()
387-
if validationNames.count == 1, let firstField = validationNames.first {
388-
await self?.options.focusedFieldOption.triggerFocus(on: firstField)
357+
if shouldFocus {
358+
await self?.focusFieldAfterTrigger(names: validationNames, errorFields: errors.errorFields)
389359
}
390360
}
391361
}
392362
return isValid
393363
}
394364

395365
@discardableResult
396-
public func trigger(name: FieldName...) async -> Bool {
397-
await trigger(names: name)
366+
public func trigger(name: FieldName..., shouldFocus: Bool = false) async -> Bool {
367+
await trigger(names: name, shouldFocus: shouldFocus)
398368
}
399369
}
400370

@@ -404,64 +374,36 @@ extension FormControl {
404374
return
405375
}
406376
if let resolver = options.resolver {
407-
let isValid: Bool
408377
let result = await resolver(instantFormState.formValues, options.context, Array(fields.keys))
409378
switch result {
410379
case .success(let formValues):
411-
isValid = true
380+
instantFormState.isValid = true
412381
instantFormState.formValues.unioned(formValues)
382+
await syncFormState()
413383
case .failure(let e):
414-
isValid = false
415-
currentErrorNotifyTask?.cancel()
416-
if options.delayErrorInNanoseconds == 0 || isValid {
417-
currentErrorNotifyTask = nil
418-
instantFormState.errors = e
419-
} else {
420-
let delayErrorInNanoseconds = options.delayErrorInNanoseconds
421-
currentErrorNotifyTask = Task { [weak self] in
422-
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
423-
self?.instantFormState.errors = e
424-
await self?.syncFormState()
425-
await self?.focusError(with: e)
426-
}
427-
}
384+
await onResultPostUpdateValid(e, isValid: false)
428385
}
429-
instantFormState.isValid = isValid
430-
} else {
431-
let (isValid, errors) = await withTaskGroup(of: KeyValidationResult.self) { group in
432-
var errors = instantFormState.errors
433-
for (key, field) in fields {
434-
group.addTask {
435-
let (isValid, messages) = await field.computeMessages()
436-
return KeyValidationResult(key: key, isValid: isValid, messages: messages)
437-
}
438-
}
439-
for await keyResult in group {
440-
errors.setMessages(name: keyResult.key, messages: keyResult.messages, isValid: keyResult.isValid)
441-
if keyResult.isValid {
442-
continue
443-
}
444-
group.cancelAll()
445-
return (false, errors)
386+
return
387+
}
388+
let (isValid, errors) = await withTaskGroup(of: KeyValidationResult.self) { group in
389+
var errors = instantFormState.errors
390+
for (key, field) in fields {
391+
group.addTask {
392+
let (isValid, messages) = await field.computeMessages()
393+
return KeyValidationResult(key: key, isValid: isValid, messages: messages)
446394
}
447-
return (true, errors)
448395
}
449-
currentErrorNotifyTask?.cancel()
450-
if options.delayErrorInNanoseconds == 0 || isValid {
451-
currentErrorNotifyTask = nil
452-
instantFormState.errors = errors
453-
} else {
454-
let delayErrorInNanoseconds = options.delayErrorInNanoseconds
455-
currentErrorNotifyTask = Task { [weak self] in
456-
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
457-
self?.instantFormState.errors = errors
458-
await self?.syncFormState()
459-
await self?.focusError(with: errors)
396+
for await keyResult in group {
397+
errors.setMessages(name: keyResult.key, messages: keyResult.messages, isValid: keyResult.isValid)
398+
if keyResult.isValid {
399+
continue
460400
}
401+
group.cancelAll()
402+
return (false, errors)
461403
}
462-
instantFormState.isValid = isValid
404+
return (true, errors)
463405
}
464-
await syncFormState()
406+
await onResultPostUpdateValid(errors, isValid: isValid)
465407
}
466408

467409
@MainActor
@@ -500,10 +442,6 @@ private extension FormControl {
500442
options.shouldUnregister
501443
}
502444

503-
func computeResult() async -> Bool {
504-
await options.rules.isValid(value.wrappedValue)
505-
}
506-
507445
func computeMessages() async -> (Bool, [String]) {
508446
await options.rules.computeMessage(value: value.wrappedValue)
509447
}
@@ -524,13 +462,7 @@ private extension FormControl {
524462
return
525463
}
526464
Task {
527-
guard await self.trigger(name: name) else {
528-
return
529-
}
530-
guard self.options.delayErrorInNanoseconds == 0 else {
531-
return
532-
}
533-
await self.options.focusedFieldOption.triggerFocus(on: name)
465+
await self.trigger(name: name, shouldFocus: true)
534466
}
535467
}
536468
}
@@ -544,6 +476,62 @@ private extension FormControl {
544476
}
545477
return options.reValidateMode.contains(.onChange)
546478
}
479+
480+
func postHandleSubmit(isOveralValid: Bool, errors: FormError<FieldName>, isSubmitSuccessful: Bool) async {
481+
instantFormState.submissionState = .submitted
482+
instantFormState.isSubmitSuccessful = isSubmitSuccessful
483+
instantFormState.submitCount += 1
484+
await onResultPostUpdateValid(errors, isValid: isOveralValid)
485+
}
486+
487+
@MainActor
488+
func focusFieldAfterTrigger(names: [FieldName], errorFields: Set<FieldName>) {
489+
let focusField: FieldName?
490+
if names.count == 1 {
491+
focusField = names[0]
492+
if currentFocusedField != nil && currentFocusedField != focusField {
493+
return
494+
}
495+
} else {
496+
focusField = names.first(where: errorFields.contains)
497+
}
498+
guard let focusField else {
499+
return
500+
}
501+
currentFocusedField = focusField
502+
}
503+
504+
func onResultPostUpdateValid(_ errors: FormError<FieldName>, isValid: Bool) async {
505+
instantFormState.isValid = isValid
506+
currentErrorNotifyTask?.cancel()
507+
if options.delayErrorInNanoseconds == 0 || isValid {
508+
currentErrorNotifyTask = nil
509+
instantFormState.errors = errors
510+
await syncFormState()
511+
return await focusError(with: errors)
512+
}
513+
await syncFormState()
514+
let delayErrorInNanoseconds = options.delayErrorInNanoseconds
515+
currentErrorNotifyTask = Task { [weak self] in
516+
try await Task.sleep(nanoseconds: delayErrorInNanoseconds)
517+
self?.instantFormState.errors = errors
518+
await self?.syncFormState()
519+
await self?.focusError(with: errors)
520+
}
521+
}
522+
523+
@MainActor
524+
func focusError(with errors: FormError<FieldName>) {
525+
guard options.shouldFocusError else {
526+
return
527+
}
528+
let fields = fields.sorted { $0.value.fieldOrdinal < $1.value.fieldOrdinal }
529+
let firstErrorField = fields.first(where: { errors.errorFields.contains($0.key) })?.key
530+
guard let firstErrorField else {
531+
return
532+
}
533+
currentFocusedField = firstErrorField
534+
}
547535
}
548536

549537
private extension FormControl {

Sources/FormHook/Hook/UseForm.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,22 @@ public struct FormOption<FieldName> where FieldName: Hashable {
118118
private let anyFocusedFieldBinder: Any?
119119
private let onFocusField: ((FieldName) -> Void)?
120120

121+
var hasFocusedFieldBinder: Bool {
122+
anyFocusedFieldBinder != nil
123+
}
124+
125+
var focusedFieldBindingValue: FieldName? {
126+
if #available(macOS 12.0, iOS 15.0, tvOS 15.0, *) {
127+
return focusedFieldBinder?.wrappedValue
128+
}
129+
return nil
130+
}
131+
132+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
133+
var focusedFieldBinder: FocusState<FieldName?>.Binding? {
134+
anyFocusedFieldBinder as? FocusState<FieldName?>.Binding
135+
}
136+
121137
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
122138
init(_ focusedFieldBinder: FocusState<FieldName?>.Binding) {
123139
self.anyFocusedFieldBinder = focusedFieldBinder
@@ -135,7 +151,6 @@ public struct FormOption<FieldName> where FieldName: Hashable {
135151
return onFocusField(field)
136152
}
137153
if #available(macOS 12.0, iOS 15.0, tvOS 15.0, *) {
138-
let focusedFieldBinder = anyFocusedFieldBinder as? FocusState<FieldName?>.Binding
139154
focusedFieldBinder?.wrappedValue = field
140155
}
141156
}

0 commit comments

Comments
 (0)