Skip to content

Commit

Permalink
feat(validation-errors): enable explicit controller binding
Browse files Browse the repository at this point in the history
Enable developers to choose the controller the validation-errors attribute is tied to.
  • Loading branch information
dkent600 authored and jdanyow committed Jan 15, 2017
1 parent 7152a1c commit 4fbf24e
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 11 deletions.
32 changes: 21 additions & 11 deletions src/validation-errors-custom-attribute.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { bindingMode } from 'aurelia-binding';
import { Lazy } from 'aurelia-dependency-injection';
import { customAttribute } from 'aurelia-templating';
import { customAttribute, bindable } from 'aurelia-templating';
import { ValidationController } from './validation-controller';
import { ValidateResult } from './validate-result';
import { ValidationRenderer, RenderInstruction } from './validation-renderer';
Expand All @@ -10,18 +10,23 @@ export interface RenderedError {
targets: Element[];
}

@customAttribute('validation-errors', bindingMode.twoWay)
@customAttribute('validation-errors')
export class ValidationErrorsCustomAttribute implements ValidationRenderer {
public static inject = [Element, Lazy.of(ValidationController)];

public value: RenderedError[];
@bindable({ defaultBindingMode: bindingMode.oneWay })
public controller: ValidationController | null = null;

@bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay })
public errors: RenderedError[] = [];

private errorsInternal: RenderedError[] = [];

constructor(private boundaryElement: Element, private controllerAccessor: { (): ValidationController; }) {
}

public sort() {
this.errors.sort((a, b) => {
this.errorsInternal.sort((a, b) => {
if (a.targets[0] === b.targets[0]) {
return 0;
}
Expand All @@ -36,9 +41,9 @@ export class ValidationErrorsCustomAttribute implements ValidationRenderer {

public render(instruction: RenderInstruction) {
for (let { result } of instruction.unrender) {
const index = this.errors.findIndex(x => x.error === result);
const index = this.errorsInternal.findIndex(x => x.error === result);
if (index !== -1) {
this.errors.splice(index, 1);
this.errorsInternal.splice(index, 1);
}
}

Expand All @@ -48,20 +53,25 @@ export class ValidationErrorsCustomAttribute implements ValidationRenderer {
}
const targets = this.interestingElements(elements);
if (targets.length) {
this.errors.push({ error: result, targets });
this.errorsInternal.push({ error: result, targets });
}
}

this.sort();
this.value = this.errors;
this.errors = this.errorsInternal;
}

public bind() {
this.controllerAccessor().addRenderer(this);
this.value = this.errors;
if (!this.controller) {
this.controller = this.controllerAccessor();
}
// this will call render() with the side-effect of updating this.errors
this.controller.addRenderer(this);
}

public unbind() {
this.controllerAccessor().removeRenderer(this);
if (this.controller) {
this.controller.removeRenderer(this);
}
}
}
1 change: 1 addition & 0 deletions test/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function configure(config: FrameworkConfiguration) {
'./number-value',
'./registration-form',
'./trigger-form',
'./validation-errors-form-one',
'./nullable-object-form'
]);
}
40 changes: 40 additions & 0 deletions test/resources/validation-errors-form-one.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { inject } from 'aurelia-dependency-injection';
import { noView } from 'aurelia-templating';
import {
ValidationRules,
ValidationController,
ValidationControllerFactory
} from '../../src/aurelia-validation';
import {InlineViewStrategy} from 'aurelia-framework';

@noView
@inject(ValidationControllerFactory)
export class ValidationErrorsFormOne {

public controller: ValidationController;
public model: any;
public form: string;

public standardInput: HTMLInputElement;
public standardProp = '';

constructor(public controllerFactory: ValidationControllerFactory) { }

public activate(model: any) {
this.form = model.form;
this.model = model;
}

public created() {
let controller = this.model.controller();
this.controller = controller ? controller : this.controllerFactory.createForCurrentScope();
}

public getViewStrategy() {
return new InlineViewStrategy(this.form);
}
}

ValidationRules
.ensure((f: ValidationErrorsFormOne) => f.standardProp).required()
.on(ValidationErrorsFormOne);
159 changes: 159 additions & 0 deletions test/validation-errors-custom-attribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { StageComponent, ComponentTester } from 'aurelia-testing';
import { bootstrap } from 'aurelia-bootstrapper';
import { configure, blur } from './shared';
import {
ValidationControllerFactory
} from '../src/aurelia-validation';
import { Container } from 'aurelia-dependency-injection';
import { Aurelia } from 'aurelia-framework';

describe('ValidationErrorsCustomAttribute', () => {

let component: ComponentTester;
let viewModel: any;
let parentViewModel = { form: '', controller: () => { return null; }, theController: null };
let container: Container;

let stageTest = (validationErrors: string, supplyControllerToViewModel?: boolean) => {
let form: string = `<template>
<form novalidate autocomplete='off' ${validationErrors}>
<input ref='standardInput' type='text' value.bind='standardProp & validateOnBlur'>
</form>
</template>`;

parentViewModel.form = form;

component = StageComponent
.withResources()
// tslint:disable-next-line:max-line-length
.inView(`<compose containerless view-model="./dist/test/test/resources/validation-errors-form-one" model.bind="{ form: form, controller: controller }"></compose>`)
// tslint:enable-next-line:max-line-length
.boundTo( parentViewModel );

let myConfigure = (aurelia: Aurelia) => {
configure(aurelia);
container = aurelia.container;
};

component.bootstrap(myConfigure);

/*
at this point validation plugin has not yet been initialized, not until in component.create()
*/
if (supplyControllerToViewModel) {
/*
the viewmodel is going to call this in created().
at that point the validation plugin will have been initialized and bind() will
not yet have been executed.
*/
parentViewModel.controller = () => {
const factory = container.get(ValidationControllerFactory);
let controller = factory.createForCurrentScope();
parentViewModel.theController = controller;
return controller;
};
}

return <Promise<void>>component.create(<any>bootstrap)
.then(() => {
/*
we get here after the viewmodel's bind().
*/
viewModel = component.viewModel.currentViewModel;
});
};

it('sets errors given as default property', (done: () => void) => {

stageTest(`validation-errors.bind='myErrors'`)
.then(() => expect(viewModel).not.toBeNull())
.then(() => expect(viewModel.myErrors instanceof Array).toBe(true))
.then(() => expect(viewModel.myErrors.length).toBe(0))
.then(() => {
blur(viewModel.standardInput);
return new Promise<void>((resolve) => { setTimeout(() => { resolve(); }, 0); });
})
.then(() => expect(viewModel.myErrors.length).toBe(1))
.then(done)
/* tslint:disable:no-console */
.catch(e => { console.log(e.toString()); done(); });
/* tslint:enable:no-console */
});

it('sets errors given as named property', (done: () => void) => {

stageTest(`validation-errors='errors.bind:myErrors'`)
.then(() => expect(viewModel).not.toBeNull())
.then(() => expect(viewModel.myErrors instanceof Array).toBe(true))
.then(() => expect(viewModel.myErrors.length).toBe(0))
.then(() => {
blur(viewModel.standardInput);
return new Promise<void>((resolve) => { setTimeout(() => { resolve(); }, 0); });
})
.then(() => expect(viewModel.myErrors.length).toBe(1))
.then(done)
/* tslint:disable:no-console */
.catch(e => { console.log(e.toString()); done(); });
/* tslint:enable:no-console */
});

it('uses given controller', (done: () => void) => {

stageTest(`validation-errors='errors.bind:myErrors;controller.bind:controller'`, true)
.then(() => expect(viewModel).not.toBeNull())
.then(() => expect(parentViewModel.controller).not.toBeNull())
.then(() => expect(viewModel.myErrors instanceof Array).toBe(true))
.then(() => expect(viewModel.myErrors.length).toBe(0))
.then(() => {
blur(viewModel.standardInput);
return new Promise<void>((resolve) => { setTimeout(() => { resolve(); }, 0); });
})
.then(() => expect(viewModel.myErrors.length).toBe(1))
// this shows that myErrors is being written from the controller that we gave to validation-errors
.then(() => expect(viewModel.myErrors[0].error).toEqual((<any>parentViewModel).theController.errors[0]))
.then(done)
/* tslint:disable:no-console */
.catch(e => { console.log(e.toString()); done(); });
/* tslint:enable:no-console */
});

it('does nothing when given only a controller', (done: () => void) => {

stageTest(`validation-errors='controller.bind:controller'`, true)
.then(() => expect(viewModel).not.toBeNull())
.then(() => expect(parentViewModel.controller).not.toBeNull())
.then(() => expect(viewModel.myErrors).toBeUndefined())
.then(() => {
blur(viewModel.standardInput);
return new Promise<void>((resolve) => { setTimeout(() => { resolve(); }, 0); });
})
.then(() => expect(viewModel.myErrors).toBeUndefined())
.then(done)
/* tslint:disable:no-console */
.catch(e => { console.log(e.toString()); done(); });
/* tslint:enable:no-console */
});

it('does nothing when given nothing', (done: () => void) => {

stageTest(`validation-errors=''`, true)
.then(() => expect(viewModel).not.toBeNull())
.then(() => expect(parentViewModel.controller).not.toBeNull())
.then(() => expect(viewModel.myErrors).toBeUndefined())
.then(() => {
blur(viewModel.standardInput);
return new Promise<void>((resolve) => { setTimeout(() => { resolve(); }, 0); });
})
.then(() => expect(viewModel.myErrors).toBeUndefined())
.then(done)
/* tslint:disable:no-console */
.catch(e => { console.log(e.toString()); done(); });
/* tslint:enable:no-console */
});

afterEach(() => {
if (component) {
component.dispose();
}
});
});

0 comments on commit 4fbf24e

Please sign in to comment.