@@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
1515limitations under the License.
1616*/
1717
18- import React , { ReactNode } from "react" ;
18+ import React , { ReactChild , ReactNode } from "react" ;
1919import classNames from "classnames" ;
20+ import memoizeOne from "memoize-one" ;
2021
2122type Data = Pick < IFieldState , "value" | "allowEmpty" > ;
2223
@@ -40,6 +41,7 @@ interface IArgs<T, D = void> {
4041 description ?( this : T , derivedData : D , results : IResult [ ] ) : ReactNode ;
4142 hideDescriptionIfValid ?: boolean ;
4243 deriveData ?( data : Data ) : Promise < D > ;
44+ memoize ?: boolean ;
4345}
4446
4547export interface IFieldState {
@@ -60,7 +62,7 @@ export interface IValidationResult {
6062 * @param {Function } description
6163 * Function that returns a string summary of the kind of value that will
6264 * meet the validation rules. Shown at the top of the validation feedback.
63- * @param {Boolean } hideDescriptionIfValid
65+ * @param {boolean } hideDescriptionIfValid
6466 * If true, don't show the description if the validation passes validation.
6567 * @param {Function } deriveData
6668 * Optional function that returns a Promise to an object of generic type D.
@@ -75,6 +77,9 @@ export interface IValidationResult {
7577 * - `valid`: Function returning text to show when the rule is valid. Only shown if set.
7678 * - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
7779 * - `final`: A Boolean if true states that this rule will only be considered if all rules before it returned valid.
80+ * @param {boolean? } memoize
81+ * If true, will use memoization to avoid calling deriveData & rules unless the value or allowEmpty change.
82+ * Be careful to not use this if your validation is not pure and depends on other fields, such as "repeat password".
7883 * @returns {Function }
7984 * A validation function that takes in the current input value and returns
8085 * the overall validity and a feedback UI that can be rendered for more detail.
@@ -84,73 +89,87 @@ export default function withValidation<T = void, D = void>({
8489 hideDescriptionIfValid,
8590 deriveData,
8691 rules,
87- } : IArgs < T , D > ) {
88- return async function onValidate (
92+ memoize,
93+ } : IArgs < T , D > ) : ( fieldState : IFieldState ) => Promise < IValidationResult > {
94+ let checkRules = async function (
8995 this : T ,
90- { value, focused, allowEmpty = true } : IFieldState ,
91- ) : Promise < IValidationResult > {
92- if ( ! value && allowEmpty ) {
93- return { } ;
94- }
95-
96- const data = { value, allowEmpty } ;
97- // We know that if deriveData is set then D will not be undefined
98- const derivedData : D = ( await deriveData ?. call ( this , data ) ) as D ;
99-
96+ data : Data ,
97+ derivedData : D ,
98+ ) : Promise < [ valid : boolean , results : IResult [ ] ] > {
10099 const results : IResult [ ] = [ ] ;
101100 let valid = true ;
102- if ( rules ?. length ) {
103- for ( const rule of rules ) {
104- if ( ! rule . key || ! rule . test ) {
105- continue ;
106- }
101+ for ( const rule of rules ) {
102+ if ( ! rule . key || ! rule . test ) {
103+ continue ;
104+ }
107105
108- if ( ! valid && rule . final ) {
109- continue ;
110- }
106+ if ( ! valid && rule . final ) {
107+ continue ;
108+ }
109+
110+ if ( rule . skip ?. call ( this , data , derivedData ) ) {
111+ continue ;
112+ }
111113
112- if ( rule . skip ?. call ( this , data , derivedData ) ) {
114+ // We're setting `this` to whichever component holds the validation
115+ // function. That allows rules to access the state of the component.
116+ const ruleValid : boolean = await rule . test . call ( this , data , derivedData ) ;
117+ valid = valid && ruleValid ;
118+ if ( ruleValid && rule . valid ) {
119+ // If the rule's result is valid and has text to show for
120+ // the valid state, show it.
121+ const text = rule . valid . call ( this , derivedData ) ;
122+ if ( ! text ) {
113123 continue ;
114124 }
115-
116- // We're setting `this` to whichever component holds the validation
117- // function. That allows rules to access the state of the component.
118- const ruleValid : boolean = await rule . test . call ( this , data , derivedData ) ;
119- valid = valid && ruleValid ;
120- if ( ruleValid && rule . valid ) {
121- // If the rule's result is valid and has text to show for
122- // the valid state, show it.
123- const text = rule . valid . call ( this , derivedData ) ;
124- if ( ! text ) {
125- continue ;
126- }
127- results . push ( {
128- key : rule . key ,
129- valid : true ,
130- text,
131- } ) ;
132- } else if ( ! ruleValid && rule . invalid ) {
133- // If the rule's result is invalid and has text to show for
134- // the invalid state, show it.
135- const text = rule . invalid . call ( this , derivedData ) ;
136- if ( ! text ) {
137- continue ;
138- }
139- results . push ( {
140- key : rule . key ,
141- valid : false ,
142- text,
143- } ) ;
125+ results . push ( {
126+ key : rule . key ,
127+ valid : true ,
128+ text,
129+ } ) ;
130+ } else if ( ! ruleValid && rule . invalid ) {
131+ // If the rule's result is invalid and has text to show for
132+ // the invalid state, show it.
133+ const text = rule . invalid . call ( this , derivedData ) ;
134+ if ( ! text ) {
135+ continue ;
144136 }
137+ results . push ( {
138+ key : rule . key ,
139+ valid : false ,
140+ text,
141+ } ) ;
145142 }
146143 }
147144
145+ return [ valid , results ] ;
146+ } ;
147+
148+ // We have to memoize it in chunks as `focused` can change frequently, but it isn't passed to these methods
149+ if ( memoize ) {
150+ if ( deriveData ) deriveData = memoizeOne ( deriveData , isDataEqual ) ;
151+ checkRules = memoizeOne ( checkRules , isDerivedDataEqual ) ;
152+ }
153+
154+ return async function onValidate (
155+ this : T ,
156+ { value, focused, allowEmpty = true } : IFieldState ,
157+ ) : Promise < IValidationResult > {
158+ if ( ! value && allowEmpty ) {
159+ return { } ;
160+ }
161+
162+ const data = { value, allowEmpty } ;
163+ // We know that if deriveData is set then D will not be undefined
164+ const derivedData = ( await deriveData ?. call ( this , data ) ) as D ;
165+ const [ valid , results ] = await checkRules . call ( this , data , derivedData ) ;
166+
148167 // Hide feedback when not focused
149168 if ( ! focused ) {
150169 return { valid } ;
151170 }
152171
153- let details ;
172+ let details : ReactNode | undefined ;
154173 if ( results && results . length ) {
155174 details = (
156175 < ul className = "mx_Validation_details" >
@@ -170,15 +189,15 @@ export default function withValidation<T = void, D = void>({
170189 ) ;
171190 }
172191
173- let summary ;
192+ let summary : ReactNode | undefined ;
174193 if ( description && ( details || ! hideDescriptionIfValid ) ) {
175194 // We're setting `this` to whichever component holds the validation
176195 // function. That allows rules to access the state of the component.
177196 const content = description . call ( this , derivedData , results ) ;
178197 summary = content ? < div className = "mx_Validation_description" > { content } </ div > : undefined ;
179198 }
180199
181- let feedback ;
200+ let feedback : ReactChild | undefined ;
182201 if ( summary || details ) {
183202 feedback = (
184203 < div className = "mx_Validation" >
@@ -194,3 +213,11 @@ export default function withValidation<T = void, D = void>({
194213 } ;
195214 } ;
196215}
216+
217+ function isDataEqual ( [ a ] : [ Data ] , [ b ] : [ Data ] ) : boolean {
218+ return a . value === b . value && a . allowEmpty === b . allowEmpty ;
219+ }
220+
221+ function isDerivedDataEqual ( [ a1 , a2 ] : [ Data , any ] , [ b1 , b2 ] : [ Data , any ] ) : boolean {
222+ return a2 === b2 && isDataEqual ( [ a1 ] , [ b1 ] ) ;
223+ }
0 commit comments