Search code examples
javascriptvalidationaurelia

Validation works first time, but not subsequently


I'm not sure what's missing here. When the site initially loads and I try to submit an empty form, validation kicks in the validation result correctly indicates valid == false. I then attempt to validate again, anywhere on the site, and validation always returns valid == true. If I refresh the browser, again it works the first time but not afterward.

Inspecting my ValidationController instance (this.validation) on the initial attempt, bindings is filled in. On Subsequent attempts, bindings is empty.

Here is an example of how my validation is set up.

create.ts

import { autoinject} from "aurelia-framework";
import { ValidationController, ValidationRules } from "aurelia-validation";
import { DefaultValidationControllerFactory } from "../../lib/validation/default-validation-controller-factory";
import { Customer } from "../../lib/models/Customer";

@autoinject
export class CreateForm {
    private validation: ValidationController;

    public customer = new Customer();

    constructor(validationControllerFactory: DefaultValidationControllerFactory) {
        this.validation = validationControllerFactory.createForCurrentScope();
    }

    public attached() {
        ValidationRules
            .ensure((o: Customer) => o.firstName)
            .required()
            .ensure((o: Customer) => o.lastName)
            .required()
            .ensure((o: Customer) => o.facebookName)
            .required()
            .ensure((o: Customer) => o.state)
            .required()
            .on(this.customer);
    }

    public createCustomer() {
        this.isBusy = true;

        return this.validation
            .validate()
            .then(result => {
                if (result.valid) {
                    // ...
                }
            })
            .finally(() => {
                this.isBusy = false;
            });
    }
}

create.html

<template>
    <div busy.bind="isBusy" class="form content-area">
        <div class="row">
            <div class="col-xs-12 col-sm-6">
                <div class="row">
                    <div class="col-xs-6">
                        <div class="form-group">
                            <label>First Name</label>
                            <input value.bind="customer.firstName & validate" type="text" class="form-control" />
                        </div>
                    </div>

                    <div class="col-xs-6">
                        <div class="form-group">
                            <label>Last Name</label>
                            <input value.bind="customer.lastName & validate" type="text" class="form-control" />
                        </div>
                    </div>
                </div>

                <div class="row">
                    <div class="col-xs-12">
                        <div class="form-group">
                            <label>Email Address</label>
                            <input value.bind="customer.emailAddress & validate" type="text" class="form-control" />
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

default-validation-controller-factory.ts

import { autoinject } from "aurelia-framework";
import { Validator, ValidationController, ValidationControllerFactory, ValidationRules } from "aurelia-validation";
import { DefaultValidationRenderer } from "./default-validation-renderer";

@autoinject
export class DefaultValidationControllerFactory {
    private validationControllerFactory: ValidationControllerFactory;
    private validationRenderer: DefaultValidationRenderer;

    constructor(validationControllerFactory: ValidationControllerFactory, validationRenderer: DefaultValidationRenderer) {
        this.validationControllerFactory = validationControllerFactory;
        this.validationRenderer = validationRenderer;
    }

    public create(validator?: Validator): ValidationController {
        let validationController = this.validationControllerFactory.create(validator);

        this.setDefaults(validationController);

        return validationController;
    }

    public createForCurrentScope(validator?: Validator): ValidationController {
        let validationController = this.validationControllerFactory.createForCurrentScope(validator);

        this.setDefaults(validationController);

        return validationController;
    }

    private setDefaults(validationController: ValidationController) {
        validationController.addRenderer(this.validationRenderer);
    }
}

default-validation-renderer.ts

import { inject } from "aurelia-dependency-injection";
import { ValidationRenderer, RenderInstruction } from "aurelia-validation";

const ValidationErrorClassName = "validation-error";

@inject(Element)
export class DefaultValidationRenderer implements ValidationRenderer {
    private boundaryElement: HTMLElement;

    constructor(boundaryElement: HTMLElement) {
        this.boundaryElement = boundaryElement;
    }

    public render(instruction: RenderInstruction) {
        for (let item of instruction.unrender) {
            for (let target of item.elements) {
                let messages = target.parentElement.getElementsByClassName(ValidationErrorClassName);

                for (let i = 0; i < messages.length; i++) {
                    let message = messages[i];

                    target.parentElement.removeChild(message);
                }
            }
        }

        for (let item of instruction.render.filter(o => !o.result.valid)) {
            for (let target of item.elements) {
                let message = target.ownerDocument.createElement("div");

                message.classList.add(ValidationErrorClassName);
                message.innerHTML = item.result.message;
                target.parentNode.insertBefore(message, target.nextSibling);
            }
        }
    }
}

Solution

  • The problem is your DefaultValidationControllerFactory. Replace it for:

    export class DefaultValidationControllerFactory {
    
     public static get(container: Container) {
        return new DefaultValidationControllerFactory(container);
      }
    
      constructor(private container: Container) { }
    
      public create(validator?: Validator) {
        if (!validator) {
          validator = this.container.get(Validator) as Validator;
        }
        return new ValidationController(validator);
      }
    
      public createForCurrentScope(validator?: Validator) {
        const controller = this.create(validator);
        controller.addRenderer(new DefaultValidationRenderer());
        this.container.registerInstance(ValidationController, controller);
        return controller;
      }
    }
    
    (DefaultValidationControllerFactory as any)['protocol:aurelia:resolver'] = true;