Skip to content

Commit 38025f8

Browse files
authored
Merge pull request #66 from topcoder-platform/PROD-501_link
PROD-501 url validation - PROD-265_work-intake
2 parents 74eb470 + ff79a91 commit 38025f8

File tree

12 files changed

+101
-42
lines changed

12 files changed

+101
-42
lines changed

src/lib/form/Form.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Dispatch, FormEvent, SetStateAction, useState } from 'react'
1+
import { Dispatch, FocusEvent, FormEvent, SetStateAction, useState } from 'react'
22

33
import { Button } from '../button'
44
import '../styles/index.scss'
@@ -10,7 +10,7 @@ import {
1010
formInitializeValues,
1111
formReset,
1212
formSubmitAsync,
13-
formValidateAndUpdate,
13+
formValidateAndUpdateAsync,
1414
} from './form-functions'
1515
import { InputText, InputTextarea } from './form-input'
1616
import { FormInputModel } from './form-input.model'
@@ -36,12 +36,29 @@ const Form: <ValueType extends any, RequestType extends any>(props: FormProps<Va
3636
const [formKey, setFormKey]: [number, Dispatch<SetStateAction<number>>]
3737
= useState<number>(Date.now())
3838

39-
function onChange(event: FormEvent<HTMLFormElement>): void {
40-
const isValid: boolean = formValidateAndUpdate(event, formDef.inputs)
39+
async function onBlur(event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>): Promise<void> {
40+
const inputDef: FormInputModel = formGetInputModel(props.formDef.inputs, event.target.name)
41+
inputDef.validateOnBlur
42+
?.forEach(async validator => {
43+
if (!inputDef.error) {
44+
inputDef.error = await validator(event.target.value, event.target.form?.elements, inputDef.dependentField)
45+
setFormDef({ ...formDef })
46+
}
47+
})
48+
}
49+
50+
async function onChange(event: FormEvent<HTMLFormElement>): Promise<void> {
51+
const isValid: boolean = await formValidateAndUpdateAsync(event, formDef.inputs)
4152
setFormDef({ ...formDef })
4253
setDisableSave(!isValid)
4354
}
4455

56+
function onFocus(event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>): void {
57+
const inputDef: FormInputModel = formGetInputModel(props.formDef.inputs, event.target.name)
58+
inputDef.dirtyOrTouched = true
59+
setFormDef({ ...formDef })
60+
}
61+
4562
function onReset(): void {
4663
setFormDef({ ...formDef })
4764
formReset(props.formDef.inputs, props.formValues)
@@ -73,17 +90,23 @@ const Form: <ValueType extends any, RequestType extends any>(props: FormProps<Va
7390
.map((inputModel, index) => {
7491
switch (inputModel.type) {
7592
case 'textarea':
76-
return <InputTextarea
77-
{...inputModel}
78-
key={inputModel.name}
79-
tabIndex={inputModel.notTabbable ? -1 : index + 1}
80-
value={inputModel.value}
81-
/>
93+
return (
94+
<InputTextarea
95+
{...inputModel}
96+
key={inputModel.name}
97+
onBlur={onBlur}
98+
onFocus={onFocus}
99+
tabIndex={inputModel.notTabbable ? -1 : index + 1}
100+
value={inputModel.value}
101+
/>
102+
)
82103
default:
83104
return (
84105
<InputText
85106
{...inputModel}
86107
key={inputModel.name}
108+
onBlur={onBlur}
109+
onFocus={onFocus}
87110
tabIndex={inputModel.notTabbable ? -1 : index + 1}
88111
type={inputModel.type || 'text'}
89112
value={inputModel.value}

src/lib/form/form-functions/form.functions.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function getInputModel(inputs: ReadonlyArray<FormInputModel>, fieldName:
2626

2727
export function initializeValues<T>(inputs: ReadonlyArray<FormInputModel>, formValues?: T): void {
2828
inputs
29-
.filter(input => !input.dirty)
29+
.filter(input => !input.dirtyOrTouched)
3030
.forEach(input => {
3131
input.value = !!(formValues as any)?.hasOwnProperty(input.name)
3232
? (formValues as any)[input.name]
@@ -37,7 +37,7 @@ export function initializeValues<T>(inputs: ReadonlyArray<FormInputModel>, formV
3737
export function reset(inputs: ReadonlyArray<FormInputModel>, formValue?: any): void {
3838
inputs
3939
.forEach(inputDef => {
40-
inputDef.dirty = false
40+
inputDef.dirtyOrTouched = false
4141
inputDef.error = undefined
4242
inputDef.value = formValue?.[inputDef.name]
4343
})
@@ -56,7 +56,7 @@ export async function submitAsync<T, R>(
5656
setDisableButton(true)
5757

5858
// if there are no dirty fields, display a message and stop submitting
59-
const dirty: FormInputModel | undefined = inputs.find(fieldDef => !!fieldDef.dirty)
59+
const dirty: FormInputModel | undefined = inputs.find(fieldDef => !!fieldDef.dirtyOrTouched)
6060
if (!dirty) {
6161
toast.info('No changes detected.')
6262
return
@@ -66,7 +66,7 @@ export async function submitAsync<T, R>(
6666
const formValues: HTMLFormControlsCollection = (event.target as HTMLFormElement).elements
6767

6868
// if there are any validation errors, display a message and stop submitting
69-
const isValid: boolean = validate(inputs, formValues, true)
69+
const isValid: boolean = await validateAsync(inputs, formValues, true)
7070
if (!isValid) {
7171
toast.error('Changes could not be saved. Please resolve errors.')
7272
return Promise.reject(ErrorMessage.submit)
@@ -85,30 +85,30 @@ export async function submitAsync<T, R>(
8585
})
8686
}
8787

88-
export function validateAndUpdate(event: FormEvent<HTMLFormElement>, inputs: ReadonlyArray<FormInputModel>): boolean {
88+
export async function validateAndUpdateAsync(event: FormEvent<HTMLFormElement>, inputs: ReadonlyArray<FormInputModel>): Promise<boolean> {
8989

9090
const input: HTMLInputElement = (event.target as HTMLInputElement)
9191
// set the input def info
9292
const inputDef: FormInputModel = getInputModel(inputs, input.name)
93-
inputDef.dirty = true
93+
inputDef.dirtyOrTouched = true
9494
inputDef.value = input.value
9595

9696
// validate the form
9797
const formElements: HTMLFormControlsCollection = (input.form as HTMLFormElement).elements
98-
const isValid: boolean = validate(inputs, formElements)
98+
const isValid: boolean = await validateAsync(inputs, formElements)
9999

100100
return isValid
101101
}
102102

103-
function validate(inputs: ReadonlyArray<FormInputModel>, formElements: HTMLFormControlsCollection, formDirty?: boolean): boolean {
103+
async function validateAsync(inputs: ReadonlyArray<FormInputModel>, formElements: HTMLFormControlsCollection, formDirty?: boolean): Promise<boolean> {
104104
const errors: ReadonlyArray<FormInputModel> = inputs
105105
.filter(formInputDef => {
106106
formInputDef.error = undefined
107-
formInputDef.dirty = formInputDef.dirty || !!formDirty
108-
formInputDef.validators
109-
.forEach(validator => {
107+
formInputDef.dirtyOrTouched = formInputDef.dirtyOrTouched || !!formDirty
108+
formInputDef.validateOnChange
109+
?.forEach(async validator => {
110110
if (!formInputDef.error) {
111-
formInputDef.error = validator(formInputDef.value, formElements, formInputDef.dependentField)
111+
formInputDef.error = await validator(formInputDef.value, formElements, formInputDef.dependentField)
112112
}
113113
})
114114
return !!formInputDef.error

src/lib/form/form-functions/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ export {
55
initializeValues as formInitializeValues,
66
reset as formReset,
77
submitAsync as formSubmitAsync,
8-
validateAndUpdate as formValidateAndUpdate,
8+
validateAndUpdateAsync as formValidateAndUpdateAsync,
99
} from './form.functions'

src/lib/form/form-input.model.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { ValidatorFn } from './validator-functions'
2+
13
export interface FormInputModel {
24
readonly dependentField?: string
3-
dirty?: boolean
5+
dirtyOrTouched?: boolean
46
disabled?: boolean
57
error?: string
68
readonly hint?: string
@@ -10,6 +12,7 @@ export interface FormInputModel {
1012
readonly placeholder?: string
1113
readonly preventAutocomplete?: boolean
1214
readonly type: 'password' | 'text' | 'textarea'
13-
readonly validators: Array<(value: string | undefined, formValues?: HTMLFormControlsCollection, otherField?: string) => string | undefined>
15+
readonly validateOnBlur?: ValidatorFn
16+
readonly validateOnChange?: ValidatorFn
1417
value?: string
1518
}

src/lib/form/form-input/form-field-wrapper/FormFieldWrapper.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const optionalHint: string = '(optional)'
99

1010
interface FormFieldWrapperProps {
1111
readonly children: ReactNode
12-
readonly dirty?: boolean
12+
readonly dirtyOrTouched?: boolean
1313
readonly disabled: boolean
1414
readonly error?: string
1515
readonly hint?: string
@@ -21,7 +21,7 @@ const FormFieldWrapper: FC<FormFieldWrapperProps> = (props: FormFieldWrapperProp
2121

2222
const [focusStyle, setFocusStyle]: [string | undefined, Dispatch<SetStateAction<string | undefined>>] = useState<string | undefined>()
2323

24-
const showError: boolean = !!props.error && !!props.dirty
24+
const showError: boolean = !!props.error && !!props.dirtyOrTouched
2525
const formFieldClasses: string = classNames(
2626
styles['form-field'],
2727
props.disabled ? styles.disabled : undefined,

src/lib/form/form-input/input-text/InputText.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
1-
import { FC } from 'react'
1+
import { FC, FocusEvent } from 'react'
22

3+
import { ValidatorFn } from '../../validator-functions'
34
import { FormFieldWrapper } from '../form-field-wrapper'
45

56
import styles from './InputText.module.scss'
67

78
export const optionalHint: string = '(optional)'
89

910
interface InputTextProps {
10-
readonly dirty?: boolean
11+
readonly dirtyOrTouched?: boolean
1112
readonly disabled?: boolean
1213
readonly error?: string
1314
readonly hint?: string
1415
readonly label?: string
1516
readonly name: string
17+
readonly onBlur: (event: FocusEvent<HTMLInputElement>) => void
18+
readonly onFocus: (event: FocusEvent<HTMLInputElement>) => void
1619
readonly placeholder?: string
1720
readonly preventAutocomplete?: boolean
1821
readonly tabIndex: number
1922
readonly type: 'password' | 'text'
23+
readonly validateOnBlur?: ValidatorFn
2024
readonly value?: string | number
2125
}
2226

2327
const InputText: FC<InputTextProps> = (props: InputTextProps) => {
28+
2429
return (
2530
<FormFieldWrapper
26-
dirty={!!props.dirty}
31+
dirtyOrTouched={!!props.dirtyOrTouched}
2732
disabled={!!props.disabled}
2833
error={props.error}
2934
hint={props.hint}
@@ -35,6 +40,8 @@ const InputText: FC<InputTextProps> = (props: InputTextProps) => {
3540
className={styles['form-input-text']}
3641
defaultValue={props.value}
3742
disabled={!!props.disabled}
43+
onBlur={props.onBlur}
44+
onFocus={props.onFocus}
3845
name={props.name}
3946
placeholder={props.placeholder}
4047
tabIndex={props.tabIndex}

src/lib/form/form-input/input-textarea/InputTextarea.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { FC } from 'react'
1+
import { FC, FocusEvent } from 'react'
22

3+
import { ValidatorFn } from '../../validator-functions'
34
import { FormFieldWrapper } from '../form-field-wrapper'
45

56
import styles from './InputTextarea.module.scss'
@@ -11,16 +12,19 @@ interface InputTextareaProps {
1112
readonly hint?: string
1213
readonly label?: string
1314
readonly name: string
15+
readonly onBlur: (event: FocusEvent<HTMLTextAreaElement>) => void
16+
readonly onFocus: (event: FocusEvent<HTMLTextAreaElement>) => void
1417
readonly placeholder?: string
1518
readonly preventAutocomplete?: boolean
1619
readonly tabIndex: number
20+
readonly validateOnBlur?: ValidatorFn
1721
readonly value?: string | number
1822
}
1923

2024
const InputTextarea: FC<InputTextareaProps> = (props: InputTextareaProps) => {
2125
return (
2226
<FormFieldWrapper
23-
dirty={!!props.dirty}
27+
dirtyOrTouched={!!props.dirty}
2428
disabled={!!props.disabled}
2529
error={props.error}
2630
hint={props.hint}
@@ -33,6 +37,8 @@ const InputTextarea: FC<InputTextareaProps> = (props: InputTextareaProps) => {
3337
defaultValue={props.value}
3438
disabled={!!props.disabled}
3539
name={props.name}
40+
onBlur={props.onBlur}
41+
onFocus={props.onFocus}
3642
placeholder={props.placeholder}
3743
tabIndex={props.tabIndex}
3844
/>

src/lib/form/validator-functions/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ export {
55
password as validatorPassword,
66
required as validatorRequired,
77
requiredIfOther as validatorRequiredIfOther,
8+
url as validatorUrl,
89
} from './validator.functions'
10+
export
11+
// tslint:disable-next-line: no-unused-expression
12+
type { ValidatorFn } from './validator.functions'

src/lib/form/validator-functions/validator.functions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ export function requiredIfOther(value: string | undefined, formElements?: HTMLFo
9090
return `required when ${getOtherFieldLabel(otherField, otherFieldName)} is not blank`
9191
}
9292

93+
export function url(value: string | undefined): string | undefined {
94+
95+
// if there's no value, there's nothing to check
96+
if (!value) {
97+
return undefined
98+
}
99+
100+
const urlRegex: RegExp = /\b(https?|ftp|file):\/\/[\-A-Za-z0-9+&@#\/%?=~_|!:,.;]*[\-A-Za-z0-9+&@#\/%=~_|]/
101+
return !urlRegex.test(value) ? 'invalid url' : undefined
102+
}
103+
104+
export type ValidatorFn = Array<(value: string | undefined, formValues?: HTMLFormControlsCollection, otherField?: string)
105+
=> string | undefined | Promise<string | undefined>>
106+
93107
function getOtherField(formElements?: HTMLFormControlsCollection, otherFieldName?: string): HTMLInputElement {
94108

95109
// if there are no form values or an other field name, we have a problem

src/tools/work-intake/work-intake-form.config.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
FormDefinition,
33
inputOptionalHint,
44
validatorRequired,
5+
validatorUrl,
56
} from '../../lib'
67

78
export const workIntakeTitle: string = 'Define your work'
@@ -21,7 +22,7 @@ export const workIntakeDef: FormDefinition = {
2122
label: 'Project Title',
2223
name: 'title',
2324
type: 'text',
24-
validators: [
25+
validateOnChange: [
2526
validatorRequired,
2627
],
2728
},
@@ -31,13 +32,15 @@ export const workIntakeDef: FormDefinition = {
3132
name: 'data',
3233
placeholder: 'Paste a link',
3334
type: 'text',
34-
validators: [],
35+
validateOnChange: [
36+
validatorUrl,
37+
],
3538
},
3639
{
3740
label: 'What would you like to learn?',
3841
name: 'description',
3942
type: 'textarea',
40-
validators: [
43+
validateOnChange: [
4144
validatorRequired,
4245
],
4346
},

0 commit comments

Comments
 (0)