Skip to content

Commit 61c4d61

Browse files
authored
Support multiple validators to Incubator.TextField (#927)
* Support multiple validators to Incubator.TextField * Code review fixes
1 parent 0d0c186 commit 61c4d61

File tree

14 files changed

+158
-25
lines changed

14 files changed

+158
-25
lines changed

demo/src/screens/componentScreens/IncubatorTextFieldScreen.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ export default class TextFieldScreen extends Component {
103103
label="Email"
104104
placeholder="Enter email"
105105
enableErrors
106-
validationMessage="Email is invalid"
106+
validationMessage={['Email is required', 'Email is invalid']}
107107
validationMessageStyle={Typography.text90R}
108108
validationMessagePosition={errorPosition}
109-
validate={'email'}
109+
validate={['required', 'email']}
110110
validateOnChange
111111
// validateOnStart
112112
// validateOnBlur

generatedTypes/incubator/TextField/FieldContext.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export declare type ContextType = {
44
isFocused: boolean;
55
hasValue: boolean;
66
isValid: boolean;
7+
failingValidatorIndex?: number;
78
disabled: boolean;
89
};
910
declare const FieldContext: import("react").Context<ContextType>;
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
import { ContextType } from './FieldContext';
2-
import { ColorType } from './types';
2+
import { ColorType, Validator } from './types';
33
export declare function getColorByState(color: ColorType, context?: ContextType): string | undefined;
4+
export declare function validate(value?: string, validator?: Validator | Validator[]): [boolean, number?];
5+
export declare function getRelevantValidationMessage(validationMessage: string | string[] | undefined, failingValidatorIndex: undefined | number): string | undefined;

generatedTypes/incubator/TextField/ValidationMessage.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface ValidationMessageProps {
88
/**
99
* The validation message to display when field is invalid (depends on validate)
1010
*/
11-
validationMessage?: string;
11+
validationMessage?: string | string[];
1212
/**
1313
* Custom style for the validation message
1414
*/

generatedTypes/incubator/TextField/index.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import React from 'react';
22
import { ViewStyle, TextStyle } from 'react-native';
33
import { ButtonPropTypes } from '../../components/button';
4-
import { ValidationMessagePosition } from './types';
4+
import { ValidationMessagePosition, Validator } from './types';
55
import { InputProps } from './Input';
66
import { ValidationMessageProps } from './ValidationMessage';
77
import { LabelProps } from './Label';
8-
import { Validator } from './useFieldState';
98
import { FloatingPlaceholderProps } from './FloatingPlaceholder';
109
import { CharCounterProps } from './CharCounter';
1110
interface TextFieldProps extends InputProps, LabelProps, FloatingPlaceholderProps, ValidationMessageProps, Omit<CharCounterProps, 'maxLength'> {

generatedTypes/incubator/TextField/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import formValidators from './validators';
12
export declare type ColorType = string | {
23
default?: string;
34
focus?: string;
@@ -8,3 +9,4 @@ export declare enum ValidationMessagePosition {
89
TOP = "top",
910
BOTTOM = "bottom"
1011
}
12+
export declare type Validator = Function | keyof typeof formValidators;

generatedTypes/incubator/TextField/useFieldState.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { TextInputProps } from 'react-native';
2-
import validators from './validators';
3-
export declare type Validator = Function | keyof typeof validators;
2+
import { Validator } from './types';
43
export interface FieldStateProps extends TextInputProps {
54
validateOnStart?: boolean;
65
validateOnChange?: boolean;
@@ -19,5 +18,6 @@ export default function useFieldState({ validate, validateOnBlur, validateOnChan
1918
hasValue: boolean;
2019
isValid: boolean;
2120
isFocused: boolean;
21+
failingValidatorIndex: number | undefined;
2222
};
2323
};

src/incubator/TextField/FieldContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ export type ContextType = {
55
isFocused: boolean;
66
hasValue: boolean;
77
isValid: boolean;
8+
failingValidatorIndex?: number;
89
disabled: boolean;
910
};
1011

1112
const FieldContext = createContext<ContextType>({
1213
isFocused: false,
1314
hasValue: false,
1415
isValid: true,
16+
failingValidatorIndex: undefined,
1517
disabled: false
1618
});
1719

src/incubator/TextField/Presenter.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {Colors} from './../../style';
21
import _ from 'lodash';
2+
import {Colors} from './../../style';
33
import {ContextType} from './FieldContext';
4-
import {ColorType} from './types';
4+
import {ColorType, Validator} from './types';
5+
import formValidators from './validators';
56

67
export function getColorByState(color: ColorType, context?: ContextType) {
78
let finalColor: string | undefined = Colors.grey10;
@@ -21,3 +22,49 @@ export function getColorByState(color: ColorType, context?: ContextType) {
2122

2223
return finalColor;
2324
}
25+
26+
export function validate(
27+
value?: string,
28+
validator?: Validator | Validator[]
29+
): [boolean, number?] {
30+
if (_.isUndefined(validator)) {
31+
return [true, undefined];
32+
}
33+
34+
let _isValid = true;
35+
let _failingValidatorIndex;
36+
const _validators = _.isArray(validator) ? validator : [validator];
37+
38+
_.forEach(_validators, (validator: Validator, index) => {
39+
if (_.isFunction(validator)) {
40+
_isValid = validator(value);
41+
} else if (_.isString(validator)) {
42+
_isValid = _.invoke(formValidators, validator, value);
43+
}
44+
45+
if (!_isValid) {
46+
_failingValidatorIndex = index;
47+
return false;
48+
}
49+
});
50+
51+
return [_isValid, _failingValidatorIndex];
52+
}
53+
54+
export function getRelevantValidationMessage(
55+
validationMessage: string | string[] | undefined,
56+
failingValidatorIndex: undefined | number
57+
) {
58+
if (
59+
_.isUndefined(failingValidatorIndex) ||
60+
_.isUndefined(validationMessage)
61+
) {
62+
return;
63+
}
64+
65+
if (_.isString(validationMessage)) {
66+
return validationMessage;
67+
} else if (_.isArray(validationMessage)) {
68+
return validationMessage[failingValidatorIndex];
69+
}
70+
}

src/incubator/TextField/ValidationMessage.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, {useContext} from 'react';
22
import {TextStyle, StyleSheet} from 'react-native';
33
import Text from '../../components/text';
44
import FieldContext from './FieldContext';
5+
import {getRelevantValidationMessage} from './Presenter';
56

67
export interface ValidationMessageProps {
78
/**
@@ -11,7 +12,7 @@ export interface ValidationMessageProps {
1112
/**
1213
* The validation message to display when field is invalid (depends on validate)
1314
*/
14-
validationMessage?: string;
15+
validationMessage?: string | string[];
1516
/**
1617
* Custom style for the validation message
1718
*/
@@ -31,9 +32,14 @@ const ValidationMessage = ({
3132
return null;
3233
}
3334

35+
const relevantValidationMessage = getRelevantValidationMessage(
36+
validationMessage,
37+
context.failingValidatorIndex
38+
);
39+
3440
return (
3541
<Text red30 style={[styles.validationMessage, validationMessageStyle]}>
36-
{context.isValid ? '' : validationMessage}
42+
{context.isValid ? '' : relevantValidationMessage}
3743
</Text>
3844
);
3945
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as uut from '../Presenter';
2+
3+
describe('TextField:Presenter', () => {
4+
describe('validate', () => {
5+
it('should return true if validator is undefined', () => {
6+
expect(uut.validate('value', undefined)).toEqual([true, undefined]);
7+
});
8+
9+
it('should validate email', () => {
10+
const validator = 'email';
11+
12+
expect(uut.validate('value', validator)).toEqual([false, 0]);
13+
expect(uut.validate('', validator)).toEqual([false, 0]);
14+
expect(uut.validate('test@mail', validator)).toEqual([false, 0]);
15+
expect(uut.validate('[email protected]', validator)).toEqual([
16+
true,
17+
undefined
18+
]);
19+
});
20+
21+
it('should validate required', () => {
22+
const validator = 'required';
23+
24+
expect(uut.validate('', validator)).toEqual([false, 0]);
25+
expect(uut.validate(undefined, validator)).toEqual([false, 0]);
26+
expect(uut.validate('value', validator)).toEqual([true, undefined]);
27+
});
28+
29+
it('should validate a function validator', () => {
30+
const validator = (value) => value.length > 3;
31+
32+
expect(uut.validate('', validator)).toEqual([false, 0]);
33+
expect(uut.validate('abc', validator)).toEqual([false, 0]);
34+
expect(uut.validate('abcd', validator)).toEqual([true, undefined]);
35+
});
36+
37+
it('should validate both required and email', () => {
38+
const validator = ['required', 'email'];
39+
40+
expect(uut.validate('', validator)).toEqual([false, 0]);
41+
expect(uut.validate('value', validator)).toEqual([false, 1]);
42+
expect(uut.validate('[email protected]', validator)).toEqual([
43+
true,
44+
undefined
45+
]);
46+
});
47+
});
48+
49+
describe('getValidationMessage', () => {
50+
it('should return undefined when there is no validationMessage', () => {
51+
expect(uut.getRelevantValidationMessage(undefined, 0)).toBeUndefined();
52+
});
53+
54+
it('should return the relevant validation message when there is a single message', () => {
55+
expect(uut.getRelevantValidationMessage('error message', 0)).toBe(
56+
'error message'
57+
);
58+
expect(
59+
uut.getRelevantValidationMessage('error message', undefined)
60+
).toBeUndefined();
61+
});
62+
63+
it('should return the relevant validation message when there are multiple messages', () => {
64+
const messages = ['Field is required', 'Email is invalid'];
65+
expect(uut.getRelevantValidationMessage(messages, 0)).toBe(messages[0]);
66+
expect(uut.getRelevantValidationMessage(messages, 1)).toBe(messages[1]);
67+
expect(
68+
uut.getRelevantValidationMessage(messages, undefined)
69+
).toBeUndefined();
70+
});
71+
});
72+
});

src/incubator/TextField/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import {
77
} from '../../commons/new';
88
import View from '../../components/view';
99
import {ButtonPropTypes} from '../../components/button';
10-
import {ValidationMessagePosition} from './types';
10+
import {ValidationMessagePosition, Validator} from './types';
1111
import Input, {InputProps} from './Input';
1212
import AccessoryButton from './AccessoryButton';
1313
import ValidationMessage, {ValidationMessageProps} from './ValidationMessage';
1414
import Label, {LabelProps} from './Label';
1515
import FieldContext from './FieldContext';
16-
import useFieldState, {Validator/* , FieldStateProps */} from './useFieldState';
16+
import useFieldState, {/* , FieldStateProps */} from './useFieldState';
1717
import FloatingPlaceholder, {
1818
FloatingPlaceholderProps
1919
} from './FloatingPlaceholder';

src/incubator/TextField/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import formValidators from './validators';
2+
13
export type ColorType =
24
| string
35
| {
@@ -11,3 +13,6 @@ export enum ValidationMessagePosition {
1113
TOP = 'top',
1214
BOTTOM = 'bottom'
1315
}
16+
17+
18+
export type Validator = Function | keyof typeof formValidators;

src/incubator/TextField/useFieldState.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {useCallback, useState, useEffect, useMemo} from 'react';
22
import {TextInputProps} from 'react-native';
33
import _ from 'lodash';
4-
import validators from './validators';
5-
6-
export type Validator = Function | keyof typeof validators;
4+
import * as Presenter from './Presenter';
5+
import {Validator} from './types';
76

87
export interface FieldStateProps extends TextInputProps {
98
validateOnStart?: boolean;
@@ -25,6 +24,7 @@ export default function useFieldState({
2524
const [value, setValue] = useState(props.value);
2625
const [isFocused, setIsFocused] = useState(false);
2726
const [isValid, setIsValid] = useState(true);
27+
const [failingValidatorIndex, setFailingValidatorIndex] = useState<number | undefined>(undefined);
2828

2929
useEffect(() => {
3030
if (validateOnStart) {
@@ -34,14 +34,11 @@ export default function useFieldState({
3434

3535
const validateField = useCallback(
3636
(valueToValidate = value) => {
37-
let _isValid = true;
38-
if (_.isFunction(validate)) {
39-
_isValid = validate(valueToValidate);
40-
} else if (_.isString(validate)) {
41-
_isValid = _.invoke(validators, validate, valueToValidate);
42-
}
37+
38+
const [_isValid, _failingValidatorIndex] = Presenter.validate(valueToValidate, validate);
4339

4440
setIsValid(_isValid);
41+
setFailingValidatorIndex(_failingValidatorIndex);
4542
},
4643
[value, validate]
4744
);
@@ -78,8 +75,8 @@ export default function useFieldState({
7875
);
7976

8077
const fieldState = useMemo(() => {
81-
return {value, hasValue: !_.isEmpty(value), isValid, isFocused};
82-
}, [value, isFocused, isValid]);
78+
return {value, hasValue: !_.isEmpty(value), isValid, isFocused, failingValidatorIndex};
79+
}, [value, isFocused, isValid, failingValidatorIndex]);
8380

8481
return {
8582
onFocus,

0 commit comments

Comments
 (0)