Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions client/shared/src/util/useInputValidation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { compact, head } from 'lodash'
import { useMemo, useState, useRef } from 'react'
import { compact, head, noop } from 'lodash'
import { useMemo, useState, useRef, useCallback } from 'react'
import { concat, EMPTY, Observable, of, zip } from 'rxjs'
import { catchError, map, switchMap, tap, debounceTime } from 'rxjs/operators'
import { useEventObservable } from './useObservable'
Expand Down Expand Up @@ -36,22 +36,32 @@ export type InputValidationState = { value: string } & (
| { kind: 'INVALID'; reason: string }
)

/**
* Options for overriding input state programmatically. If `overrideState` is called without
* these options, the input will be cleared and not validated.
*/
interface OverrideOptions {
/** The value to set input state to */
value: string

/** Whether to validate the new value */
validate?: boolean
}

/**
* React hook to manage validation of a single form input field.
* `useInputValidation` helps with coordinating the constraint validation API
* and custom synchronous and asynchronous validators.
*
* @param options Config object that declares sync + async validators
* @param initialValue
*
* @returns
*/
export function useInputValidation(
options: ValidationOptions
): [
InputValidationState,
(change: React.ChangeEvent<HTMLInputElement>) => void,
React.MutableRefObject<HTMLInputElement | null>
React.MutableRefObject<HTMLInputElement | null>,
(overrideOptions: OverrideOptions) => void
] {
const inputReference = useRef<HTMLInputElement>(null)

Expand All @@ -66,7 +76,29 @@ export function useInputValidation(

const [nextInputChangeEvent] = useEventObservable(validationPipeline)

return [inputState, nextInputChangeEvent, inputReference]
// TODO(tj): Move control of state to consumer
const overrideState = useCallback(
(overrideOptions: OverrideOptions) => {
// clear custom validity
inputReference.current?.setCustomValidity('')

// clear React state
setInputState({
kind: overrideOptions?.validate ? 'LOADING' : 'NOT_VALIDATED',
value: overrideOptions?.value ?? '',
})

if (overrideOptions?.validate) {
nextInputChangeEvent({
preventDefault: noop,
target: { value: overrideOptions.value },
})
}
},
[nextInputChangeEvent]
)

return [inputState, nextInputChangeEvent, inputReference, overrideState]
}

/**
Expand Down
3 changes: 2 additions & 1 deletion client/web/src/user/settings/emails/AddUserEmailForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Status = undefined | 'loading' | ErrorLike
export const AddUserEmailForm: FunctionComponent<Props> = ({ user, className, onDidAdd, history }) => {
const [statusOrError, setStatusOrError] = useState<Status>()

const [emailState, nextEmailFieldChange, emailInputReference] = useInputValidation(
const [emailState, nextEmailFieldChange, emailInputReference, overrideEmailState] = useInputValidation(
useMemo(
() => ({
synchronousValidators: [],
Expand Down Expand Up @@ -57,6 +57,7 @@ export const AddUserEmailForm: FunctionComponent<Props> = ({ user, className, on
)

eventLogger.log('NewUserEmailAddressAdded')
overrideEmailState({ value: '' })
setStatusOrError(undefined)

if (onDidAdd) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,8 @@ export const UserSettingsEmailsPage: FunctionComponent<Props> = ({ user, history
</div>
)}

<AddUserEmailForm
key={emails.length}
className="mt-4"
user={user.id}
onDidAdd={fetchEmails}
history={history}
/>
{/* re-fetch emails on onDidAdd to guarantee correct state */}
<AddUserEmailForm className="mt-4" user={user.id} onDidAdd={fetchEmails} history={history} />
<hr className="my-4" />
{statusOrError === 'loaded' && (
<SetUserPrimaryEmailForm user={user.id} emails={emails} onDidSet={fetchEmails} history={history} />
Expand Down