@@ -11,6 +11,7 @@ import compose from '../../libs/compose';
11
11
import { withNetwork } from '../OnyxProvider' ;
12
12
import stylePropTypes from '../../styles/stylePropTypes' ;
13
13
import networkPropTypes from '../networkPropTypes' ;
14
+ import CONST from '../../CONST' ;
14
15
15
16
const propTypes = {
16
17
/** A unique Onyx key identifying the form */
@@ -98,19 +99,75 @@ function getInitialValueByType(valueType) {
98
99
}
99
100
}
100
101
101
- function FormProvider ( { validate, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, onSubmit, ...rest } ) {
102
+ function FormProvider ( { validate, formID , shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, onSubmit, ...rest } ) {
102
103
const inputRefs = useRef ( null ) ;
103
104
const touchedInputs = useRef ( { } ) ;
104
105
const [ inputValues , setInputValues ] = useState ( { } ) ;
105
106
const [ errors , setErrors ] = useState ( { } ) ;
107
+ const hasServerError = useMemo ( ( ) => Boolean ( formState ) && ! _ . isEmpty ( formState . errors ) , [ formState ] ) ;
106
108
107
109
const onValidate = useCallback (
108
- ( values ) => {
110
+ ( values , shouldClearServerError = true ) => {
111
+ const trimmedStringValues = { } ;
112
+ _ . each ( values , ( inputValue , inputID ) => {
113
+ if ( _ . isString ( inputValue ) ) {
114
+ trimmedStringValues [ inputID ] = inputValue . trim ( ) ;
115
+ } else {
116
+ trimmedStringValues [ inputID ] = inputValue ;
117
+ }
118
+ } ) ;
119
+
120
+ if ( shouldClearServerError ) {
121
+ FormActions . setErrors ( formID , null ) ;
122
+ }
123
+ FormActions . setErrorFields ( formID , null ) ;
124
+
109
125
const validateErrors = validate ( values ) || { } ;
110
- setErrors ( validateErrors ) ;
111
- return validateErrors ;
126
+
127
+ // Validate the input for html tags. It should supercede any other error
128
+ _ . each ( trimmedStringValues , ( inputValue , inputID ) => {
129
+ // If the input value is empty OR is non-string, we don't need to validate it for HTML tags
130
+ if ( ! inputValue || ! _ . isString ( inputValue ) ) {
131
+ return ;
132
+ }
133
+ const foundHtmlTagIndex = inputValue . search ( CONST . VALIDATE_FOR_HTML_TAG_REGEX ) ;
134
+ const leadingSpaceIndex = inputValue . search ( CONST . VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX ) ;
135
+
136
+ // Return early if there are no HTML characters
137
+ if ( leadingSpaceIndex === - 1 && foundHtmlTagIndex === - 1 ) {
138
+ return ;
139
+ }
140
+
141
+ const matchedHtmlTags = inputValue . match ( CONST . VALIDATE_FOR_HTML_TAG_REGEX ) ;
142
+ let isMatch = _ . some ( CONST . WHITELISTED_TAGS , ( r ) => r . test ( inputValue ) ) ;
143
+ // Check for any matches that the original regex (foundHtmlTagIndex) matched
144
+ if ( matchedHtmlTags ) {
145
+ // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed.
146
+ for ( let i = 0 ; i < matchedHtmlTags . length ; i ++ ) {
147
+ const htmlTag = matchedHtmlTags [ i ] ;
148
+ isMatch = _ . some ( CONST . WHITELISTED_TAGS , ( r ) => r . test ( htmlTag ) ) ;
149
+ if ( ! isMatch ) {
150
+ break ;
151
+ }
152
+ }
153
+ }
154
+ // Add a validation error here because it is a string value that contains HTML characters
155
+ validateErrors [ inputID ] = 'common.error.invalidCharacter' ;
156
+ } ) ;
157
+
158
+ if ( ! _ . isObject ( validateErrors ) ) {
159
+ throw new Error ( 'Validate callback must return an empty object or an object with shape {inputID: error}' ) ;
160
+ }
161
+
162
+ const touchedInputErrors = _ . pick ( validateErrors , ( inputValue , inputID ) => Boolean ( touchedInputs . current [ inputID ] ) ) ;
163
+
164
+ if ( ! _ . isEqual ( errors , touchedInputErrors ) ) {
165
+ setErrors ( touchedInputErrors ) ;
166
+ }
167
+
168
+ return touchedInputErrors ;
112
169
} ,
113
- [ validate ] ,
170
+ [ errors , formID , validate ] ,
114
171
) ;
115
172
116
173
/**
@@ -186,6 +243,18 @@ function FormProvider({validate, shouldValidateOnBlur, shouldValidateOnChange, c
186
243
propsToParse . onTouched ( event ) ;
187
244
}
188
245
} ,
246
+ onPress : ( event ) => {
247
+ setTouchedInput ( inputID ) ;
248
+ if ( _ . isFunction ( propsToParse . onPress ) ) {
249
+ propsToParse . onPress ( event ) ;
250
+ }
251
+ } ,
252
+ onPressIn : ( event ) => {
253
+ setTouchedInput ( inputID ) ;
254
+ if ( _ . isFunction ( propsToParse . onPressIn ) ) {
255
+ propsToParse . onPressIn ( event ) ;
256
+ }
257
+ } ,
189
258
onBlur : ( event ) => {
190
259
// Only run validation when user proactively blurs the input.
191
260
if ( Visibility . isVisible ( ) && Visibility . hasFocus ( ) ) {
@@ -195,7 +264,7 @@ function FormProvider({validate, shouldValidateOnBlur, shouldValidateOnChange, c
195
264
setTimeout ( ( ) => {
196
265
setTouchedInput ( inputID ) ;
197
266
if ( shouldValidateOnBlur ) {
198
- onValidate ( inputValues ) ;
267
+ onValidate ( inputValues , ! hasServerError ) ;
199
268
}
200
269
} , 200 ) ;
201
270
}
@@ -228,7 +297,7 @@ function FormProvider({validate, shouldValidateOnBlur, shouldValidateOnChange, c
228
297
} ,
229
298
} ;
230
299
} ,
231
- [ errors , formState , inputValues , onValidate , setTouchedInput , shouldValidateOnBlur , shouldValidateOnChange ] ,
300
+ [ errors , formState , hasServerError , inputValues , onValidate , setTouchedInput , shouldValidateOnBlur , shouldValidateOnChange ] ,
232
301
) ;
233
302
const value = useMemo ( ( ) => ( { registerInput} ) , [ registerInput ] ) ;
234
303
@@ -237,6 +306,7 @@ function FormProvider({validate, shouldValidateOnBlur, shouldValidateOnChange, c
237
306
{ /* eslint-disable react/jsx-props-no-spreading */ }
238
307
< FormWrapper
239
308
{ ...rest }
309
+ formID = { formID }
240
310
onSubmit = { submit }
241
311
inputRefs = { inputRefs }
242
312
errors = { errors }
0 commit comments