Skip to content

Feature/access other areas #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions __tests__/Area.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import React from 'react';
import { mount } from 'enzyme';
import { Validator } from '@/Validator';
import { ValidatorArea, ValidatorAreaProps } from '@/components/ValidatorArea';
import ValidatorProvider, { ValidatorProviderProps } from '@/components/ValidatorProvider';
import { ProviderScope } from '@/ProviderScope';

const tick = () => {
return new Promise(resolve => {
setTimeout(resolve, 0);
})
}

describe('test ValidatorProvider', () => {
beforeEach(() => {
Expand Down Expand Up @@ -111,4 +119,124 @@ describe('test ValidatorProvider', () => {
area.find('input').simulate('blur');
expect(mockFn).toBeCalled();
});

it('should get all input refs from the provider', async () => {
Validator.extend('test_all', (validator: Validator) => ({
passed(): boolean {
return validator.refs().length === 2;
},
message(): string {
return 'test';
}
}))
const mockFn = jest.fn();

const provider = mount<ValidatorProvider, ValidatorProviderProps>(
<ValidatorProvider rules="test_all">
{({ validate }: ProviderScope) => (
<>
<ValidatorArea name="test1">
<input value="" />
</ValidatorArea>
<ValidatorArea>
<input value="" name="test2" />
</ValidatorArea>
<button onClick={() => validate(mockFn)} />
</>
)}
</ValidatorProvider>
);

provider.find('button').simulate('click');
await tick();
expect(mockFn).toHaveBeenCalled()
});

it('should get spcific input refs from the provider', async () => {
Validator.extend('test_specific', (validator: Validator) => ({
passed(): boolean {
return validator.refs('test1').length === 2
&& validator.refs('test2').length === 1;
},
message(): string {
return 'test';
}
}))
const mockFn = jest.fn();

const provider = mount<ValidatorProvider, ValidatorProviderProps>(
<ValidatorProvider rules="test_specific">
{({ validate }: ProviderScope) => (
<>
<ValidatorArea name="test1">
<input value="" />
<input value="" />
</ValidatorArea>
<ValidatorArea>
<input value="" name="test2" />
</ValidatorArea>
<button onClick={() => validate(mockFn)} />
</>
)}
</ValidatorProvider>
);

provider.find('button').simulate('click');
await tick();
expect(mockFn).toHaveBeenCalled()
});

it('should return empty array when undefined area name is fetched', async () => {
Validator.extend('test_not_existing', (validator: Validator) => ({
passed(): boolean {
return validator.refs('not_existing').length === 0;
},
message(): string {
return 'test';
}
}))
const mockFn = jest.fn();

const provider = mount<ValidatorProvider, ValidatorProviderProps>(
<ValidatorProvider rules="test_not_existing">
{({ validate }: ProviderScope) => (
<>
<ValidatorArea name="test1">
<input value="" />
</ValidatorArea>
<ValidatorArea>
<input value="" name="test2" />
</ValidatorArea>
<button onClick={() => validate(mockFn)} />
</>
)}
</ValidatorProvider>
);

provider.find('button').simulate('click');
await tick();
expect(mockFn).toHaveBeenCalled();
});

it('should not be able to get all refs when not wrapped in provider', () => {
Validator.extend('no_other_areas', (validator: Validator) => ({
passed(): boolean {
return validator.refs('not_existing').length === 0
&& validator.refs().length === 0;
},
message(): string {
return 'test';
}
}))
const mockFn = jest.fn();

const area = mount<ValidatorArea, ValidatorAreaProps>(
<ValidatorArea rules="no_other_areas">
<input name="test" onBlur={mockFn} />
</ValidatorArea>
);

area.find('input').simulate('blur');
expect(mockFn).toBeCalled();
});
})
17 changes: 16 additions & 1 deletion __tests__/Validator.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Rule } from '../src/Rule';
import { Validator } from '../src';
import { Validator } from '@/Validator';

describe('test validator', () => {
beforeEach(() => {
Expand Down Expand Up @@ -103,5 +103,20 @@ describe('test validator', () => {
it('should merge rules', () => {
const rules = Validator.mergeRules(['rule_one', 'rule_two'], 'rule_tree|rule_four', ['rule_five|rule_six']);
expect(rules.length).toBe(6);
});

it('should throw an error when trying to get area when not in area', () => {
const throws = () => {
const validator = new Validator(
[
document.createElement<'input'>('input')
],
[],
'test'
);
validator.getArea();
}

expect(() => throws()).toThrowError('Areas are only available when validating React components.')
})
});
15 changes: 13 additions & 2 deletions src/Rule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { ValidationElement } from './ValidationElement';
import { Validator } from '@/Validator';
import { ValidationElement } from '@/ValidationElement';

export type Rule = {
/**
* Function to access validator using the rule
*/
export type RuleFunction = (validator: Validator) => RuleObject;

/**
* Object structure rules must implement
*/
export type RuleObject = {
/**
* Returns whether the rule passed with the given element(s)
*/
Expand All @@ -10,3 +19,5 @@ export type Rule = {
*/
message(): string;
}

export type Rule = RuleObject | RuleFunction;
46 changes: 44 additions & 2 deletions src/Validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
IntlCache,
IntlShape
} from '@formatjs/intl'
import { Rule } from './Rule';
import { Rule, RuleFunction, RuleObject } from '@/Rule';
import { ValidationElement } from '@/ValidationElement';
import { RuleOptions } from '@/RuleOptions';
import { capitalize } from '@/utils/utils';
import { ValidatorArea } from '@/components/ValidatorArea';

export class Validator {
/**
Expand Down Expand Up @@ -45,6 +46,11 @@ export class Validator {
*/
private intl: IntlShape<string>;

/**
* Validator area used to access other areas and the provider
*/
private area?: ValidatorArea;

public constructor(
elements: ValidationElement[],
rules: RuleOptions,
Expand Down Expand Up @@ -86,13 +92,22 @@ export class Validator {
.length;
}

/**
* Indicated whether a given rule name is a rule function
*/
private static isRuleFunction(rule: string): boolean {
return typeof Validator.rules[rule] === 'function';
}

/**
* Validate a specific rule
*/
private validateRule(rule: string): boolean {
const [ruleName, ruleArgs = ''] = rule.split(':');
if (Validator.hasRule(ruleName)) {
const ruleObj = Validator.rules[ruleName];
const ruleObj: RuleObject = Validator.isRuleFunction(ruleName)
? (Validator.rules[ruleName] as RuleFunction)(this) : Validator.rules[ruleName] as RuleObject;

const ruleArgsArray = ruleArgs.split(',');

if(!ruleObj.passed(this.elements, ...ruleArgsArray)) {
Expand Down Expand Up @@ -126,6 +141,33 @@ export class Validator {
return this.errors;
}

/**
* Sets the current area
*/
public setArea(area: ValidatorArea): Validator {
this.area = area;

return this;
}

/**
* Gets the area where this validator instance is used
*/
public getArea(): ValidatorArea {
if (this.area) {
return this.area;
}

throw new Error('Areas are only available when validating React components.');
}

/**
* Gets a list of validation element refs, optionally specified by area name
*/
public refs(name?: string): ValidationElement[] {
return this.getArea().context.getRefs(name);
}

/**
* Merges rules from different sources into one array
*/
Expand Down
5 changes: 4 additions & 1 deletion src/ValidatorContext.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';
import { RuleOptions } from '@/RuleOptions';
import { ValidatorArea } from '@/components/ValidatorArea';
import { ValidationElement } from '@/ValidationElement';

export interface ValidatorContextProps {
rules: RuleOptions;
addArea: (name: string, ref: ValidatorArea) => void;
getRefs: (name?: string) => ValidationElement[];
}

export const ValidatorContext = React.createContext<ValidatorContextProps>({
rules: [],
addArea: () => undefined
addArea: () => undefined,
getRefs: () => []
});
43 changes: 32 additions & 11 deletions src/components/ValidatorArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,49 @@ interface ValidatorAreaComponentsProps {
}

export class ValidatorArea extends React.Component<ValidatorAreaProps, ValidatorAreaState> {
/**
* @inheritDoc
*/
public static contextType = ValidatorContext;

/**
* @inheritDoc
*/
public context!: React.ContextType<typeof ValidatorContext>;

/**
* References to elements within the area to be validated
*/
private inputRefs: ValidationElement[] = [];

/**
* Indicates whether the area is dirty
*/
private dirty = false;

/**
* @inheritDoc
*/
public readonly state: ValidatorAreaState = {
errors: []
}

/**
* Default props when not provided in the component
*/
public static defaultProps: Partial<ValidatorAreaProps> = {
rules: []
}

/**
* @inheritDoc
*/
public componentDidMount(): void {
const { addArea } = this.context;

addArea(this.getName(), this);
}

/**
* Validate the area, or a given element when provided
*/
Expand All @@ -58,6 +88,7 @@ export class ValidatorArea extends React.Component<ValidatorAreaProps, Validator
rules,
ref ? ref.getAttribute('name') : this.getName()
);
validator.setArea(this);

this.dirty = !validator.validate();

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

/**
* @inheritDoc
*/
public componentDidMount(): void {
const { addArea } = this.context;

addArea(this.getName(), this);
}

private getName(): string {
if (this.inputRefs.length === 1 && this.inputRefs[0].getAttribute('name')) {
return this.inputRefs[0].getAttribute('name') as string;
Expand Down Expand Up @@ -135,7 +157,7 @@ export class ValidatorArea extends React.Component<ValidatorAreaProps, Validator
this.validate(ref);
},
ref: (node: ValidationElement) => {
if (node) {
if (node && !this.inputRefs.includes(node)) {
ref = node;
this.inputRefs.push(ref);
}
Expand Down Expand Up @@ -172,7 +194,6 @@ export class ValidatorArea extends React.Component<ValidatorAreaProps, Validator
*/
public render(): React.ReactNode {
let { children } = this.props;
this.inputRefs = [];

if (typeof children === 'function') {
children = (children as (scope: AreaScope) => React.ReactNode)(
Expand Down
Loading