Skip to content

Commit

Permalink
[Minor] add .cancel() callback
Browse files Browse the repository at this point in the history
  • Loading branch information
ealush committed Jun 11, 2019
1 parent b7f8c10 commit 3b9363f
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 82 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Changed
- [Major] Lowercased library name when imported on global object.
- [Major] Renamed `validationErrors` and `validationWarnings` output properties to `errors` and `warnings`.
- [Patch] Guarantee that `.done()` callbacks only run once.

### Added
- [Minor] `.after()` callback that can run after a specific field finished execution.
- [Minor] New size rules (`lessThan`, `greaterThan`, `lessThanOrEquals`, `greaterThanOrEquals`, `numberEquals`, `numberNotEquals`).
- [Minor] New size rules (`longerThan`, `shorterThan`, `longerThanOrEquals`, `shorterThanOrEquals`, `lengthEquals`, `lengthNotEquals`).
- [Minor] New content rules (`equals`, `notEquals`).
- [Minor] Support returning promise from test callback for async tests.
- [Minor] Add cancel callback.

### Removed
- [Major] Removed output properties: `hasValidationErrors`, `hasValidationWarnings`.
Expand Down
3 changes: 2 additions & 1 deletion config/test-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const excludeFromResult = [
'getErrors',
'hasErrors',
'hasWarnings',
'getWarnings'
'getWarnings',
'cancel'
];

global.PASSABLE_VERSION = require('../package.json').version;
84 changes: 56 additions & 28 deletions dist/passable.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/passable.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/passable.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/passable.min.js.map

Large diffs are not rendered by default.

50 changes: 33 additions & 17 deletions documentation/getting_started/callbacks.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# The `.after()` callback
# `.after()`

> Since 6.4.0
> Since 7.0.0
The after callback is a function that can be chained to a passable suite and allows invoking a callback whenever a certain field has finished running, regardless of whether it passed or failed. It accepts two arguments: `fieldName` and `callback`. You may chain multiple callbacks to the same field.

Expand Down Expand Up @@ -36,7 +36,7 @@ passable('SendEmailForm', (test) => {
});
```

# The `.done()` callback
# `.done()`

> Since 6.1.0
Expand Down Expand Up @@ -76,23 +76,39 @@ passable('SendEmailForm', (test) => {
}).done(reportToServer).done(promptUserQuestionnaire);
```

In the example above, whenever the validation completes, the following functions will get called, with the [validation result object](./result.md) as an argument:
# `.cancel()`

> Since 7.0.0
When running your validation suite multiple times in a short amount of time - for example, when validating user inputs upon change, your async validations may finish after you already started running the suite again. This will cause the `.done()` and `.after()` callbacks of the previous run to be run in proximity to the `.done()` and `.after()` callbacks of the current run.

Depending on what you do in your callbacks, this can lead to wasteful action, or to validation state rapidly changing in front of the user's eyes.

To combat this, there's the `.cancel()` callback, which cancels any pending `.done()` and `.after()` callbacks.

You can use it in many ways, but the simplistic way to look at it is this: You need to keep track of your cancel callback in a scope that's still going to be accessible in the next run.

Example:

1.
```js
(res) => {
if (res.hasErrors()) {
showValidationErrors(res.errors)
let cancel = null;

// this is a simple event handler
const handleChange = (e) => {

if (cancel) {
cancel(); // now, if cancel already exists, it will cancel any pending callbacks
}
}
```

2.
```js
reportToServer
```
// you should ideally import your suite from somewhere else, this is here just for the demonstration
const result = passable('MyForm', (test) => {
// some async validations go here
});

3.
```js
promptUserQuestionnaire
result.done((res) => {
// do something
});

cancel = result.cancel; // save the cancel callback aside
}
```
64 changes: 41 additions & 23 deletions src/core/Passable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ class Passable {
specific: Specific;
res: PassableResult;
test: TestProvider;
pending: Array<PassableTest>;

pending = [];
pending: Array<PassableTest> = [];

/**
* Initializes a validation suite, creates a new passableResult instance and runs pending tests
Expand Down Expand Up @@ -53,7 +51,17 @@ class Passable {
* @param {String} fieldName name of the field to test against
* @return {Boolean}
*/
hasRemainingPendingTests = (fieldName: string) => this.pending.some((test) => test.fieldName === fieldName);
hasRemainingPendingTests = (fieldName?: string) => {
if (!this.pending.length) {
return false;
}

if (fieldName) {
return this.pending.some((test) => test.fieldName === fieldName);
}

return !!this.pending.length;
}

/**
* Test function passed over to the consumer.
Expand Down Expand Up @@ -104,25 +112,9 @@ class Passable {
statement: string
} = test;

let isAsync: boolean = typeof test.then === 'function';
const isAsync: boolean = typeof test.then === 'function';
let testResult: AnyValue;


if (!isAsync) {
try {
testResult = test();
} catch (e) {
testResult = false;
}

if (testResult && typeof testResult.then === 'function') {
isAsync = true;

// $FlowFixMe
test = testResult;
}
}

if (isAsync) {
this.res.markAsync(fieldName);

Expand All @@ -131,27 +123,53 @@ class Passable {
if (!this.hasRemainingPendingTests(fieldName)) {
this.res.markAsDone(fieldName);
}

if (!this.hasRemainingPendingTests()) {
this.res.markAsDone();
}
};

const fail: Function = () => {
// order is important here! fail needs to be called before `done`.
this.res.fail(fieldName, statement, severity);

if (this.pending.includes(test)) {
this.res.fail(fieldName, statement, severity);
}
done();
};

try {
// $FlowFixMe
test.then(done, fail);
} catch (e) {
fail();
fail(test);
}
} else {
try {
testResult = test();
} catch (e) {
testResult = false;
}

// if is async after all
if (testResult && typeof testResult.then === 'function') {

testResult.fieldName = fieldName;
testResult.statement = statement;
testResult.severity = severity;

// $FlowFixMe
return this.addPendingTest(testResult);
}

// explicitly false
if (testResult === false) {
this.res.fail(fieldName, statement, severity);
}

this.clearPendingTest(test);
}

this.res.bumpTestCounter(fieldName);
}

Expand Down
Loading

0 comments on commit 3b9363f

Please sign in to comment.