Skip to content

Commit 9c2a989

Browse files
Eran MachielsEranNL
Eran Machiels
andauthored
Feature/access other areas (#6)
* Added access to validator, area and provider (when used) within validation rules * Added missing doc * Minor fixes * Unneeded initial value fixes * Re-added missing null type for name Co-authored-by: Eran Machiels <[email protected]>
1 parent b6da5c5 commit 9c2a989

File tree

7 files changed

+261
-18
lines changed

7 files changed

+261
-18
lines changed

__tests__/Area.test.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import React from 'react';
22
import { mount } from 'enzyme';
33
import { Validator } from '@/Validator';
44
import { ValidatorArea, ValidatorAreaProps } from '@/components/ValidatorArea';
5+
import ValidatorProvider, { ValidatorProviderProps } from '@/components/ValidatorProvider';
6+
import { ProviderScope } from '@/ProviderScope';
7+
8+
const tick = () => {
9+
return new Promise(resolve => {
10+
setTimeout(resolve, 0);
11+
})
12+
}
513

614
describe('test ValidatorProvider', () => {
715
beforeEach(() => {
@@ -111,4 +119,124 @@ describe('test ValidatorProvider', () => {
111119
area.find('input').simulate('blur');
112120
expect(mockFn).toBeCalled();
113121
});
122+
123+
it('should get all input refs from the provider', async () => {
124+
Validator.extend('test_all', (validator: Validator) => ({
125+
passed(): boolean {
126+
return validator.refs().length === 2;
127+
},
128+
message(): string {
129+
return 'test';
130+
}
131+
}))
132+
const mockFn = jest.fn();
133+
134+
const provider = mount<ValidatorProvider, ValidatorProviderProps>(
135+
<ValidatorProvider rules="test_all">
136+
{({ validate }: ProviderScope) => (
137+
<>
138+
<ValidatorArea name="test1">
139+
<input value="" />
140+
</ValidatorArea>
141+
<ValidatorArea>
142+
<input value="" name="test2" />
143+
</ValidatorArea>
144+
<button onClick={() => validate(mockFn)} />
145+
</>
146+
)}
147+
</ValidatorProvider>
148+
);
149+
150+
provider.find('button').simulate('click');
151+
await tick();
152+
expect(mockFn).toHaveBeenCalled()
153+
});
154+
155+
it('should get spcific input refs from the provider', async () => {
156+
Validator.extend('test_specific', (validator: Validator) => ({
157+
passed(): boolean {
158+
return validator.refs('test1').length === 2
159+
&& validator.refs('test2').length === 1;
160+
},
161+
message(): string {
162+
return 'test';
163+
}
164+
}))
165+
const mockFn = jest.fn();
166+
167+
const provider = mount<ValidatorProvider, ValidatorProviderProps>(
168+
<ValidatorProvider rules="test_specific">
169+
{({ validate }: ProviderScope) => (
170+
<>
171+
<ValidatorArea name="test1">
172+
<input value="" />
173+
<input value="" />
174+
</ValidatorArea>
175+
<ValidatorArea>
176+
<input value="" name="test2" />
177+
</ValidatorArea>
178+
<button onClick={() => validate(mockFn)} />
179+
</>
180+
)}
181+
</ValidatorProvider>
182+
);
183+
184+
provider.find('button').simulate('click');
185+
await tick();
186+
expect(mockFn).toHaveBeenCalled()
187+
});
188+
189+
it('should return empty array when undefined area name is fetched', async () => {
190+
Validator.extend('test_not_existing', (validator: Validator) => ({
191+
passed(): boolean {
192+
return validator.refs('not_existing').length === 0;
193+
},
194+
message(): string {
195+
return 'test';
196+
}
197+
}))
198+
const mockFn = jest.fn();
199+
200+
const provider = mount<ValidatorProvider, ValidatorProviderProps>(
201+
<ValidatorProvider rules="test_not_existing">
202+
{({ validate }: ProviderScope) => (
203+
<>
204+
<ValidatorArea name="test1">
205+
<input value="" />
206+
</ValidatorArea>
207+
<ValidatorArea>
208+
<input value="" name="test2" />
209+
</ValidatorArea>
210+
<button onClick={() => validate(mockFn)} />
211+
</>
212+
)}
213+
</ValidatorProvider>
214+
);
215+
216+
provider.find('button').simulate('click');
217+
await tick();
218+
expect(mockFn).toHaveBeenCalled();
219+
});
220+
221+
it('should not be able to get all refs when not wrapped in provider', () => {
222+
Validator.extend('no_other_areas', (validator: Validator) => ({
223+
passed(): boolean {
224+
return validator.refs('not_existing').length === 0
225+
&& validator.refs().length === 0;
226+
},
227+
message(): string {
228+
return 'test';
229+
}
230+
}))
231+
const mockFn = jest.fn();
232+
233+
const area = mount<ValidatorArea, ValidatorAreaProps>(
234+
<ValidatorArea rules="no_other_areas">
235+
<input name="test" onBlur={mockFn} />
236+
</ValidatorArea>
237+
);
238+
239+
area.find('input').simulate('blur');
240+
expect(mockFn).toBeCalled();
241+
});
114242
})

__tests__/Validator.test.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Rule } from '../src/Rule';
2-
import { Validator } from '../src';
2+
import { Validator } from '@/Validator';
33

44
describe('test validator', () => {
55
beforeEach(() => {
@@ -103,5 +103,20 @@ describe('test validator', () => {
103103
it('should merge rules', () => {
104104
const rules = Validator.mergeRules(['rule_one', 'rule_two'], 'rule_tree|rule_four', ['rule_five|rule_six']);
105105
expect(rules.length).toBe(6);
106+
});
107+
108+
it('should throw an error when trying to get area when not in area', () => {
109+
const throws = () => {
110+
const validator = new Validator(
111+
[
112+
document.createElement<'input'>('input')
113+
],
114+
[],
115+
'test'
116+
);
117+
validator.getArea();
118+
}
119+
120+
expect(() => throws()).toThrowError('Areas are only available when validating React components.')
106121
})
107122
});

src/Rule.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
import { ValidationElement } from './ValidationElement';
1+
import { Validator } from '@/Validator';
2+
import { ValidationElement } from '@/ValidationElement';
23

3-
export type Rule = {
4+
/**
5+
* Function to access validator using the rule
6+
*/
7+
export type RuleFunction = (validator: Validator) => RuleObject;
8+
9+
/**
10+
* Object structure rules must implement
11+
*/
12+
export type RuleObject = {
413
/**
514
* Returns whether the rule passed with the given element(s)
615
*/
@@ -10,3 +19,5 @@ export type Rule = {
1019
*/
1120
message(): string;
1221
}
22+
23+
export type Rule = RuleObject | RuleFunction;

src/Validator.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import {
44
IntlCache,
55
IntlShape
66
} from '@formatjs/intl'
7-
import { Rule } from './Rule';
7+
import { Rule, RuleFunction, RuleObject } from '@/Rule';
88
import { ValidationElement } from '@/ValidationElement';
99
import { RuleOptions } from '@/RuleOptions';
1010
import { capitalize } from '@/utils/utils';
11+
import { ValidatorArea } from '@/components/ValidatorArea';
1112

1213
export class Validator {
1314
/**
@@ -45,6 +46,11 @@ export class Validator {
4546
*/
4647
private intl: IntlShape<string>;
4748

49+
/**
50+
* Validator area used to access other areas and the provider
51+
*/
52+
private area?: ValidatorArea;
53+
4854
public constructor(
4955
elements: ValidationElement[],
5056
rules: RuleOptions,
@@ -86,13 +92,22 @@ export class Validator {
8692
.length;
8793
}
8894

95+
/**
96+
* Indicated whether a given rule name is a rule function
97+
*/
98+
private static isRuleFunction(rule: string): boolean {
99+
return typeof Validator.rules[rule] === 'function';
100+
}
101+
89102
/**
90103
* Validate a specific rule
91104
*/
92105
private validateRule(rule: string): boolean {
93106
const [ruleName, ruleArgs = ''] = rule.split(':');
94107
if (Validator.hasRule(ruleName)) {
95-
const ruleObj = Validator.rules[ruleName];
108+
const ruleObj: RuleObject = Validator.isRuleFunction(ruleName)
109+
? (Validator.rules[ruleName] as RuleFunction)(this) : Validator.rules[ruleName] as RuleObject;
110+
96111
const ruleArgsArray = ruleArgs.split(',');
97112

98113
if(!ruleObj.passed(this.elements, ...ruleArgsArray)) {
@@ -126,6 +141,33 @@ export class Validator {
126141
return this.errors;
127142
}
128143

144+
/**
145+
* Sets the current area
146+
*/
147+
public setArea(area: ValidatorArea): Validator {
148+
this.area = area;
149+
150+
return this;
151+
}
152+
153+
/**
154+
* Gets the area where this validator instance is used
155+
*/
156+
public getArea(): ValidatorArea {
157+
if (this.area) {
158+
return this.area;
159+
}
160+
161+
throw new Error('Areas are only available when validating React components.');
162+
}
163+
164+
/**
165+
* Gets a list of validation element refs, optionally specified by area name
166+
*/
167+
public refs(name?: string): ValidationElement[] {
168+
return this.getArea().context.getRefs(name);
169+
}
170+
129171
/**
130172
* Merges rules from different sources into one array
131173
*/

src/ValidatorContext.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React from 'react';
22
import { RuleOptions } from '@/RuleOptions';
33
import { ValidatorArea } from '@/components/ValidatorArea';
4+
import { ValidationElement } from '@/ValidationElement';
45

56
export interface ValidatorContextProps {
67
rules: RuleOptions;
78
addArea: (name: string, ref: ValidatorArea) => void;
9+
getRefs: (name?: string) => ValidationElement[];
810
}
911

1012
export const ValidatorContext = React.createContext<ValidatorContextProps>({
1113
rules: [],
12-
addArea: () => undefined
14+
addArea: () => undefined,
15+
getRefs: () => []
1316
});

src/components/ValidatorArea.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,49 @@ interface ValidatorAreaComponentsProps {
2626
}
2727

2828
export class ValidatorArea extends React.Component<ValidatorAreaProps, ValidatorAreaState> {
29+
/**
30+
* @inheritDoc
31+
*/
2932
public static contextType = ValidatorContext;
33+
34+
/**
35+
* @inheritDoc
36+
*/
3037
public context!: React.ContextType<typeof ValidatorContext>;
38+
39+
/**
40+
* References to elements within the area to be validated
41+
*/
3142
private inputRefs: ValidationElement[] = [];
43+
44+
/**
45+
* Indicates whether the area is dirty
46+
*/
3247
private dirty = false;
3348

49+
/**
50+
* @inheritDoc
51+
*/
3452
public readonly state: ValidatorAreaState = {
3553
errors: []
3654
}
3755

56+
/**
57+
* Default props when not provided in the component
58+
*/
3859
public static defaultProps: Partial<ValidatorAreaProps> = {
3960
rules: []
4061
}
4162

63+
/**
64+
* @inheritDoc
65+
*/
66+
public componentDidMount(): void {
67+
const { addArea } = this.context;
68+
69+
addArea(this.getName(), this);
70+
}
71+
4272
/**
4373
* Validate the area, or a given element when provided
4474
*/
@@ -58,6 +88,7 @@ export class ValidatorArea extends React.Component<ValidatorAreaProps, Validator
5888
rules,
5989
ref ? ref.getAttribute('name') : this.getName()
6090
);
91+
validator.setArea(this);
6192

6293
this.dirty = !validator.validate();
6394

@@ -74,15 +105,6 @@ export class ValidatorArea extends React.Component<ValidatorAreaProps, Validator
74105
})
75106
}
76107

77-
/**
78-
* @inheritDoc
79-
*/
80-
public componentDidMount(): void {
81-
const { addArea } = this.context;
82-
83-
addArea(this.getName(), this);
84-
}
85-
86108
private getName(): string {
87109
if (this.inputRefs.length === 1 && this.inputRefs[0].getAttribute('name')) {
88110
return this.inputRefs[0].getAttribute('name') as string;
@@ -135,7 +157,7 @@ export class ValidatorArea extends React.Component<ValidatorAreaProps, Validator
135157
this.validate(ref);
136158
},
137159
ref: (node: ValidationElement) => {
138-
if (node) {
160+
if (node && !this.inputRefs.includes(node)) {
139161
ref = node;
140162
this.inputRefs.push(ref);
141163
}
@@ -172,7 +194,6 @@ export class ValidatorArea extends React.Component<ValidatorAreaProps, Validator
172194
*/
173195
public render(): React.ReactNode {
174196
let { children } = this.props;
175-
this.inputRefs = [];
176197

177198
if (typeof children === 'function') {
178199
children = (children as (scope: AreaScope) => React.ReactNode)(

0 commit comments

Comments
 (0)