Skip to content

Commit

Permalink
feat(ValidationController): events
Browse files Browse the repository at this point in the history
fixes #318
  • Loading branch information
jdanyow committed Mar 26, 2017
1 parent bf0d66d commit 57232ce
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 1 deletion.
4 changes: 4 additions & 0 deletions doc/article/en-US/validation-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ You may need to surface validation errors from other sources. Perhaps while atte

The validation controller renders errors by sending them to implementations of the `ValidationRenderer` interface. The library ships with a built-in renderer that "renders" the errors to an array property for data-binding/templating purposes. This is covered in the [displaying errors](aurelia-doc://section/11/version/1.0.0) section below. You can create your own [custom renderer](aurelia-doc://section/12/version/1.0.0) and add it to the controller's set of renderers using the `addRenderer(renderer)` method.

### Events

The validation controller has a `subscribe(callback: (event: ValidateEvent) => void)` method you can use to subscribe to validate and reset events. Callbacks will be invoked whenever the controller's validate and reset methods are called. Callbacks will be passed an instance `ValidateEvent` which contains properties you can use to determine the overall validity state as well as the result of the validate or reset invocation. Refer to the API docs for more info.

## [Validator](aurelia-doc://section/5/version/1.0.0)

`Validator` is an interface used by the `ValidationController` to do the behind-the-scenes work of validating objects and properties. The `aurelia-validation` plugin ships with an implementation of this interface called the `StandardValidator`, which knows how to evaluate rules created by `aurelia-validation`'s fluent API. When you use a `Validator` directly to validate a particular object or property, there are no UI side-effects- the validation results are not sent to the the validation renderers.
Expand Down
1 change: 1 addition & 0 deletions src/aurelia-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './get-target-dom-element';
export * from './property-info';
export * from './property-accessor-parser';
export * from './validate-binding-behavior';
export * from './validate-event';
export * from './validate-instruction';
export * from './validate-result';
export * from './validate-trigger';
Expand Down
41 changes: 41 additions & 0 deletions src/validate-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ValidateResult } from './validate-result';
import { ValidateInstruction } from './validate-instruction';
import { ControllerValidateResult } from './controller-validate-result';

export class ValidateEvent {
constructor(
/**
* The type of validate event. Either "validate" or "reset".
*/
public readonly type: 'validate' | 'reset',

/**
* The controller's current array of errors. For an array containing both
* failed rules and passed rules, use the "results" property.
*/
public readonly errors: ValidateResult[],

/**
* The controller's current array of validate results. This
* includes both passed rules and failed rules. For an array of only failed rules,
* use the "errors" property.
*/
public readonly results: ValidateResult[],

/**
* The instruction passed to the "validate" or "reset" event. Will be null when
* the controller's validate/reset method was called with no instruction argument.
*/
public readonly instruction: ValidateInstruction | null,

/**
* In events with type === "validate", this property will contain the result
* of validating the instruction (see "instruction" property). Use the controllerValidateResult
* to access the validate results specific to the call to "validate"
* (as opposed to using the "results" and "errors" properties to access the controller's entire
* set of results/errors).
*/
public readonly controllerValidateResult: ControllerValidateResult | null

) { }
}
38 changes: 38 additions & 0 deletions src/validation-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ValidateResult } from './validate-result';
import { ValidateInstruction } from './validate-instruction';
import { ControllerValidateResult } from './controller-validate-result';
import { PropertyAccessorParser, PropertyAccessor } from './property-accessor-parser';
import { ValidateEvent } from './validate-event';

/**
* Orchestrates validation.
Expand Down Expand Up @@ -51,8 +52,28 @@ export class ValidationController {
// Promise that resolves when validation has completed.
private finishValidating: Promise<any> = Promise.resolve();

private eventCallbacks: ((event: ValidateEvent) => void)[] = [];

constructor(private validator: Validator, private propertyParser: PropertyAccessorParser) { }

/**
* Subscribe to controller validate and reset events. These events occur when the
* controller's "validate"" and "reset" methods are called.
* @param callback The callback to be invoked when the controller validates or resets.
*/
public subscribe(callback: (event: ValidateEvent) => void) {
this.eventCallbacks.push(callback);
return {
dispose: () => {
const index = this.eventCallbacks.indexOf(callback);
if (index === -1) {
return;
}
this.eventCallbacks.splice(index, 1);
}
};
}

/**
* Adds an object to the set of objects that should be validated when validate is called.
* @param object The object.
Expand Down Expand Up @@ -224,6 +245,7 @@ export class ValidationController {
valid: newResults.find(x => !x.valid) === undefined,
results: newResults
};
this.invokeCallbacks(instruction, result);
return result;
})
.catch(exception => {
Expand All @@ -248,6 +270,7 @@ export class ValidationController {
const predicate = this.getInstructionPredicate(instruction);
const oldResults = this.results.filter(predicate);
this.processResultDelta('reset', oldResults, []);
this.invokeCallbacks(instruction, null);
}

/**
Expand Down Expand Up @@ -405,6 +428,21 @@ export class ValidationController {
this.validate({ object, propertyName, rules });
}
}

private invokeCallbacks(instruction: ValidateInstruction | undefined, result: ControllerValidateResult | null) {
if (this.eventCallbacks.length === 0) {
return;
}
const event = new ValidateEvent(
result ? 'validate' : 'reset',
this.errors,
this.results,
instruction || null,
result);
for (let i = 0; i < this.eventCallbacks.length; i++) {
this.eventCallbacks[i](event);
}
}
}

/**
Expand Down
29 changes: 28 additions & 1 deletion test/basic.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StageComponent, ComponentTester } from 'aurelia-testing';
import { bootstrap } from 'aurelia-bootstrapper';
import { RegistrationForm } from './resources/registration-form';
import { validateTrigger } from '../src/aurelia-validation';
import { validateTrigger, ValidateEvent } from '../src/aurelia-validation';
import { configure, blur, change } from './shared';

describe('end to end', () => {
Expand Down Expand Up @@ -206,6 +206,33 @@ describe('end to end', () => {
viewModel.controller.removeError(error3);
expect(viewModel.controller.errors.length).toBe(0);
})
// subscribe to error events
.then(() => {
let event1: ValidateEvent;
let event2: ValidateEvent;
const spy1 = jasmine.createSpy().and.callFake((event: ValidateEvent) => event1 = event);
const spy2 = jasmine.createSpy().and.callFake((event: ValidateEvent) => event2 = event);
viewModel.controller.subscribe(spy1);
viewModel.controller.subscribe(spy2);
return change(lastName, '')
.then(() => {
expect(spy1).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
expect(event1).toBe(event2);
expect(event1.errors.length).toBe(1);
spy1.calls.reset();
spy2.calls.reset();
event1 = null as any;
event2 = null as any;
})
.then(() => change(firstName, ''))
.then(() => {
expect(spy1).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
expect(event1).toBe(event2);
expect(event1.errors.length).toBe(2);
});
})

// cleanup and finish.
.then(() => component.dispose())
Expand Down

0 comments on commit 57232ce

Please sign in to comment.