Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jdanyow/ea843c24956cfffff48bb21776291f6a to your computer and use it in GitHub Desktop.
Save jdanyow/ea843c24956cfffff48bb21776291f6a to your computer and use it in GitHub Desktop.
Aurelia - bootstrap form validation feature
import {inject} from 'aurelia-dependency-injection';
import {validationRenderer} from 'aurelia-validation';
@validationRenderer
@inject(Element)
export class BootstrapFormValidationRenderer {
constructor(boundaryElement) {
this.boundaryElement = boundaryElement;
}
render(error, target) {
if (!target || !(this.boundaryElement === target || this.boundaryElement.contains(target))) {
return;
}
// tag the element so we know we rendered into it.
target.errors = (target.errors || new Map());
target.errors.set(error);
// add the has-error class to the bootstrap form-group div
const formGroup = target.querySelector('.form-group') || target.closest('.form-group');
formGroup.classList.add('has-error');
// add help-block
const message = document.createElement('span');
message.classList.add('help-block');
message.classList.add('validation-error');
message.textContent = error.message;
message.error = error;
formGroup.appendChild(message);
}
unrender(error, target) {
if (!target || !target.errors || !target.errors.has(error)) {
return;
}
target.errors.delete(error);
// remove the has-error class on the bootstrap form-group div
const formGroup = target.querySelector('.form-group') || target.closest('.form-group');
formGroup.classList.remove('has-error');
// remove all messages related to the error.
let messages = formGroup.querySelectorAll('.validation-error');
let i = messages.length;
while(i--) {
let message = messages[i];
if (message.error !== error) {
continue;
}
message.error = null;
message.remove();
}
}
}
// Polyfill for Element.closest and Element.matches
// https://github.com/jonathantneal/closest/
(function (ELEMENT) {
ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector;
ELEMENT.closest = ELEMENT.closest || function closest(selector) {
var element = this;
while (element) {
if (element.matches(selector)) {
break;
}
element = element.parentElement;
}
return element;
};
}(Element.prototype));
import {BootstrapFormValidationRenderer} from './bootstrap-form-validation-renderer';
export function configure(config) {
config.container.registerHandler(
'bootstrap-form',
container => container.get(BootstrapFormValidationRenderer));
}
@taz
Copy link

taz commented Jun 17, 2016

Consider moving unrender L41 to L53 and wrapping it in an if.

        if (i == 0) {
                formGroup.classList.remove('has-error');
            }

This ensures that the has-error class is only removed after the last error is resolved. Currently if there is more than one error, the has-error class is removed when the first one is resolved.

@tkhyn
Copy link

tkhyn commented Jul 1, 2016

Hi Jeremy I have a small issue: boundaryElement is always the root container's element (the body in my case)

Indeed, as I import bootstrap-validation as a feature in my app, the bootstrap-form handler is only registered on the root container.

Is there a way to register the bootstrap-validation resolver on every container so that boundaryElement is actually set to the element on which the validation-renderer custom attribute is applied?

Thanks in advance

@vincentw56
Copy link

vincentw56 commented Jul 6, 2016

On line 30, I changed it to this: target.parentNode.insertBefore(message, message.nextSibling);

This appends it right after the input. If you have your input in another div that isn't a form-group, this will put the message right after the input.

@WorldMaker
Copy link

Why isn't this a package? I get that maybe some people will care to customize the behavior, but certainly seems like a good default should be an npm install away. Hopefully it's just because you haven't gotten around to it?

@gheoan
Copy link

gheoan commented Aug 23, 2016

On line 52 ChildNode.prototype.remove() is called, which is not well supported in all browsers and should be polyfilled.

https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove

@toxicehc
Copy link

  message.parentElement.removeChild(message);

Tested in IE, which was generating errors before and it works.

@fracz
Copy link

fracz commented Nov 5, 2016

Sample unit test in TS with little support of JQuery:

import {BootstrapFormValidationRenderer} from "./bootstrap-form-validation-renderer";
import {RenderInstruction, RenderErrorInstruction, ValidationError} from "aurelia-validation";
import "jquery";

describe(BootstrapFormValidationRenderer.name, () => {
  let renderer: BootstrapFormValidationRenderer;
  let html: JQuery;

  class TestRenderInstruction implements RenderInstruction {
    kind;
    render: RenderErrorInstruction[] = [];
    unrender: RenderErrorInstruction[] = [];

    addRender(errorId: number, elementSelector: string): TestRenderInstruction {
      this.render.push(this.newRenderErrorInstruction(errorId, elementSelector));
      return this;
    }

    addUnrender(errorId: number, elementSelector: string): TestRenderInstruction {
      this.unrender.push(this.newRenderErrorInstruction(errorId, elementSelector));
      return this;
    }

    private newRenderErrorInstruction(errorId: number, elementSelector: string) {
      return <RenderErrorInstruction>{
        error: this.newError(errorId),
        elements: [html.find(`${elementSelector} input`)[0]]
      }
    };

    private newError(id: number): ValidationError {
      let error = new ValidationError({}, `Error ${id}`, {});
      error.id = id;
      return error;
    }
  }

  beforeEach(() => {
    html = $(`<form>
                  <div class="form-group first"><input type="text"></div>
                  <div class="form-group second"><input type="text"></div>
              </form>`);
    renderer = new BootstrapFormValidationRenderer;
  });

  it('displays an error', () => {
    renderer.render(new TestRenderInstruction().addRender(1, '.first'));
    expect(html.find(".first").hasClass('has-error')).toBeTruthy();
    expect(html.find(".second").hasClass('has-error')).toBeFalsy();
    expect(html.find(".first .help-block").length).toBe(1);
  });

  it('clears an error', () => {
    renderer.render(new TestRenderInstruction().addRender(1, '.first'));
    renderer.render(new TestRenderInstruction().addUnrender(1, '.first'));
    expect(html.find(".first").hasClass('has-error')).toBeFalsy();
    expect(html.find(".first .help-block").length).toBe(0);
  });

  it('renders many errors', () => {
    renderer.render(new TestRenderInstruction().addRender(1, '.first').addRender(2, '.first'));
    expect(html.find(".first").hasClass('has-error')).toBeTruthy();
    expect(html.find(".first .help-block").length).toBe(2);
  });

  it('renders errors next to desired fields', () => {
    renderer.render(new TestRenderInstruction().addRender(1, '.first').addRender(2, '.second'));
    expect(html.find(".first .help-block").text()).toBe('Error 1');
    expect(html.find(".second .help-block").text()).toBe('Error 2');
  });

  it('clears single error', () => {
    renderer.render(new TestRenderInstruction().addRender(1, '.first').addRender(2, '.first'));
    renderer.render(new TestRenderInstruction().addUnrender(1, '.first'));
    expect(html.find(".first").hasClass('has-error')).toBeTruthy();
    expect(html.find(".first .help-block").length).toBe(1);
    expect(html.find(".first .help-block").text()).toBe('Error 2');
  });
});

@hosseinakbarian
Copy link

i want to use BootstrapFormValidationRenderer code in aurelia framework javascript but i have this error TypeError: Object(...) is not a function
at Module../src/bootstrap-form-validation-renderer.js

i have a registration form like this

<div class="form-group">
  <label class="control-label" for="first">First Name</label>
  <input type="text" class="form-control" id="first" placeholder="First Name"
         value.bind="firstName  & validate"><span></span>
</div>

<div class="form-group">
  <label class="control-label" for="last">Last Name</label>
  <input type="text" class="form-control" id="last" placeholder="Last Name"
         value.bind="lastName & validate">
</div>

<div class="form-group">
  <label class="control-label" for="email">Email</label>
  <input type="text" class="form-control" id="email" placeholder="Email"
         value.bind="email & validate">
</div>

<button type="submit" class="btn btn-primary">Submit</button>

and a js file this

import {inject, NewInstance} from 'aurelia-dependency-injection';
import {ValidationController, validateTrigger} from 'aurelia-validation';
import {required, email} from 'aurelia-validatejs';
import {BootstrapFormValidationRenderer} from './bootstrap-form-validation-renderer';

@Inject(NewInstance.of(ValidationController), validateTrigger)
export class RegistrationForm {
@required
firstName = '';

@required
lastName = '';

@required
email = '';
constructor(controller) {
this.controller = controller;
controller.validateTrigger = validateTrigger.manual;
}

submit() {
let errors = this.controller.validate();
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment