Skip to content
This repository has been archived by the owner on Nov 24, 2018. It is now read-only.

Commit

Permalink
feat(valdrFormGroup): add new directive for valdr validated form groups
Browse files Browse the repository at this point in the history
- add new valdrFormGroup directive which sets validity state for a group of form items and is responsible for adding and removing validation messages if valdr-message is loaded
 - change class names used by valdr on form group level (default is now ng-invalid/valid instead of has-error has-success,this can be overridden by changing the properties on the valdrClasses value)
 closes #11, fixes #44, fixes #48
  • Loading branch information
Philipp Denzler committed Dec 9, 2014
1 parent f359fe8 commit 3986bf8
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 199 deletions.
8 changes: 4 additions & 4 deletions src/core/valdr-service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,19 @@ describe('valdr', function () {
// given
spyOn($rootScope, '$broadcast');
var newClass = 'is-valid';
expect(valdrClasses.valid).toBe('has-success');
expect(valdrClasses.invalid).toBe('has-error');
expect(valdrClasses.valid).toBe('ng-valid');
expect(valdrClasses.invalid).toBe('ng-invalid');

// when
valdr.setClasses({ valid: newClass });

// then
expect($rootScope.$broadcast).toHaveBeenCalledWith(valdrEvents.revalidate);
expect(valdrClasses.valid).toBe(newClass);
expect(valdrClasses.invalid).toBe('has-error');
expect(valdrClasses.invalid).toBe('ng-invalid');

// cleanup to prevent side-effects
valdr.setClasses({ valid: 'has-success' });
valdr.setClasses({ valid: 'ng-valid' });
});

});
Expand Down
109 changes: 109 additions & 0 deletions src/core/valdrFormGroup-directive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* This directive adds the validity state to a form group element surrounding valdr validated input fields.
* If valdr-messages is loaded, it also adds the validation messages as last element to the element this this
* directive is applied on.
*/
var valdrFormGroupDirectiveDefinition =
['valdrClasses', 'valdrConfig', function (valdrClasses, valdrConfig) {
return {
restrict: 'EA',
link: function (scope, element) {
if (valdrConfig.addFormGroupClass) {
element.addClass(valdrClasses.formGroup);
}
},
controller: ['$scope', '$element', function ($scope, $element) {

var formItems = [],
messageElements = {};

/**
* Checks the state of all valdr validated form items below this element.
* @returns {Object} an object containing the states of all form items in this form group
*/
var getFormGroupState = function () {

var formGroupState = {
// true if an item in this form group is currently dirty, touched and invalid
invalidDirtyTouchedGroup: false,
// true if all form items in this group are currently valid
valid: true,
// contains the validity states of all form items in this group
itemStates: []
};

angular.forEach(formItems, function (formItem) {
if (formItem.$touched && formItem.$dirty && formItem.$invalid) {
formGroupState.invalidDirtyTouchedGroup = true;
}

if (formItem.$invalid) {
formGroupState.valid = false;
}

var itemState = {
name: formItem.$name,
touched: formItem.$touched,
dirty: formItem.$dirty,
valid: formItem.$valid
};

formGroupState.itemStates.push(itemState);
});

return formGroupState;
};

/**
* Updates the classes on this element and the valdr message elements based on the validity states
* of the items in this form group.
* @param formGroupState the current state of this form group and its items
*/
var updateClasses = function (formGroupState) {
// form group state
$element.toggleClass(valdrClasses.invalidDirtyTouchedGroup, formGroupState.invalidDirtyTouchedGroup);
$element.toggleClass(valdrClasses.valid, formGroupState.valid);
$element.toggleClass(valdrClasses.invalid, !formGroupState.valid);

// valdr message states
angular.forEach(formGroupState.itemStates, function (itemState) {
var messageElement = messageElements[itemState.name];
if (messageElement) {
messageElement.toggleClass(valdrClasses.valid, itemState.valid);
messageElement.toggleClass(valdrClasses.invalid, !itemState.valid);
messageElement.toggleClass(valdrClasses.dirty, itemState.dirty);
messageElement.toggleClass(valdrClasses.pristine, !itemState.dirty);
messageElement.toggleClass(valdrClasses.touched, itemState.touched);
messageElement.toggleClass(valdrClasses.untouched, !itemState.touched);
}
});
};

$scope.$watch(getFormGroupState, updateClasses, true);

this.addFormItem = function (ngModelController) {
formItems.push(ngModelController);
};

this.removeFormItem = function (ngModelController) {
var index = formItems.indexOf(ngModelController);
if (index >= 0) {
formItems.splice(index, 1);
}
};

this.addMessageElement = function (ngModelController, messageElement) {
$element.append(messageElement);
messageElements[ngModelController.$name] = messageElement;
};

this.removeMessageElement = function (ngModelController) {
messageElements[ngModelController.$name].remove();
};

}]
};
}];

angular.module('valdr')
.directive('valdrFormGroup', valdrFormGroupDirectiveDefinition);
133 changes: 133 additions & 0 deletions src/core/valdrFormGroup-directive.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
describe('valdrFormGroup directive', function () {

// VARIABLES

var $scope, $compile, element, valdr, valdrClasses, valdrConfig, ngModelController,
personConstraints = {
'Person': {
'firstName': {
'size': {
'min': 0,
'max': 10,
'message': 'size'
}
},
'lastName': {
'size': {
'min': 0,
'max': 10,
'message': 'size'
}
}
}
};

var formGroupTemplate =
'<form valdr-type="Person" valdr-form-group>' +
'<input type="text" name="firstName" valdr-no-message ng-model="person.firstName">' +
'<input type="text" name="lastName" valdr-no-message ng-model="person.lastName">' +
'</form>';

// TEST UTILITIES

function compileTemplate(template) {
element = $compile(angular.element(template))($scope);
$scope.$digest();
}

function compileFormGroupTemplate() {
compileTemplate(formGroupTemplate);
ngModelController = element.find('input').controller('ngModel');
}

beforeEach(function () {
module('valdr');
});

beforeEach(inject(function ($rootScope, _$compile_, _valdr_, _valdrClasses_, _valdrConfig_) {
$compile = _$compile_;
valdr = _valdr_;
valdrClasses = _valdrClasses_;
valdrConfig = _valdrConfig_;

$scope = $rootScope.$new();
$scope.person = { };
valdr.addConstraints(personConstraints);
}));


describe('valdrFormGroup', function () {

beforeEach(function () {
compileFormGroupTemplate();
});

describe('form-group class', function () {

it ('should add form group class by default', function () {
expect(element.hasClass(valdrClasses.formGroup)).toBe(true);
});

it ('should not add form group class if option is disabled in valdrConfig', function () {
// given
valdrConfig.addFormGroupClass = false;

// when
compileFormGroupTemplate();

// then
expect(element.hasClass(valdrClasses.formGroup)).toBe(false);
});

});

it('should not set valid and invalidDirtyTouchedGroup classes if all items are valid', function () {
expect(element.hasClass(valdrClasses.valid)).toBe(true);
expect(element.hasClass(valdrClasses.invalid)).toBe(false);
expect(element.hasClass(valdrClasses.invalidDirtyTouchedGroup)).toBe(false);
});

it('should not set invalid class if an item is not valid', function () {
// given
$scope.person.firstName = 'This name is too long for the constraints.';

// when
$scope.$digest();

// then
expect(element.hasClass(valdrClasses.invalid)).toBe(true);
expect(element.hasClass(valdrClasses.valid)).toBe(false);
expect(element.hasClass(valdrClasses.invalidDirtyTouchedGroup)).toBe(false);
});

it('should add invalidDirtyTouchedGroup class if an input is dirty, touched and invalid', function () {
// given
$scope.person.firstName = 'This name is too long for the constraints.';
ngModelController.$invalid = true;
ngModelController.$dirty = true;
ngModelController.$touched = true;

// when
$scope.$digest();

// then
expect(element.hasClass(valdrClasses.invalid)).toBe(true);
expect(element.hasClass(valdrClasses.valid)).toBe(false);
expect(element.hasClass(valdrClasses.invalidDirtyTouchedGroup)).toBe(true);
});

it('should be valid if no form items are registered', function () {
// given
var template = '<form valdr-type="Person" valdr-form-group></form>';

// when
compileTemplate(template);

// then
expect(element.hasClass(valdrClasses.valid)).toBe(true);
expect(element.hasClass(valdrClasses.invalid)).toBe(false);
expect(element.hasClass(valdrClasses.invalidDirtyTouchedGroup)).toBe(false);
});
});

});
42 changes: 21 additions & 21 deletions src/core/valdrFormItem-directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,51 @@
* can be added to those fields.
*/
var valdrFormItemDirectiveDefinition =
['valdrEvents', 'valdr', 'valdrUtil', 'valdrClasses', function (valdrEvents, valdr, valdrUtil, valdrClasses) {
['valdrEvents', 'valdr', 'valdrUtil', 'valdrClasses', function (valdrEvents, valdr, valdrUtil) {
return {
restrict: 'E',
require: ['?^valdrType', '?^ngModel'],
require: ['?^valdrType', '?^ngModel', '?^valdrFormGroup'],
link: function (scope, element, attrs, controllers) {

var valdrTypeController = controllers[0],
ngModelController = controllers[1],
valdrFormGroupController = controllers[2],
valdrNoValidate = attrs.valdrNoValidate,
fieldName = attrs.name,
formGroupElement;
fieldName = attrs.name;

/*
Stop right here :
- if this is an <input> that's not inside of a valdr-type block
- if there is no ng-model bound to input
- if there is 'valdr-no-validate' attribute present
/**
* Don't do anything if
* - this is an <input> that's not inside of a valdr-type block
* - there is no ng-model bound to input
* - there is the 'valdr-no-validate' attribute present
*/
if (!valdrTypeController || !ngModelController || angular.isDefined(valdrNoValidate)) {
return;
}

if (valdrUtil.isEmpty(fieldName)) {
throw new Error('form element is not bound to a field name');
if (valdrFormGroupController) {
valdrFormGroupController.addFormItem(ngModelController);
}

formGroupElement = valdrUtil.findWrappingFormGroup(element);

var updateClassOnFormGroup = function (valid) {
formGroupElement.toggleClass(valdrClasses.valid, valid);
formGroupElement.toggleClass(valdrClasses.invalid, !valid);
};
if (valdrUtil.isEmpty(fieldName)) {
throw new Error('Form element with ID "' + attrs.id + '" is not bound to a field name.');
}

var updateNgModelController = function (validationResult) {
// set validity state for individual validators
angular.forEach(validationResult.validationResults, function (result) {
var validatorToken = valdrUtil.validatorNameToToken(result.validator);
ngModelController.$setValidity(validatorToken, result.valid);
});

// set overall validity state of this form item
ngModelController.$setValidity('valdr', validationResult.valid);
ngModelController.valdrViolations = validationResult.violations;
};

var validate = function (modelValue) {
var validationResult = valdr.validate(valdrTypeController.getType(), fieldName, modelValue);
updateNgModelController(validationResult);
updateClassOnFormGroup(validationResult.valid);
return validationResult.valid;
};

Expand All @@ -59,11 +58,12 @@ var valdrFormItemDirectiveDefinition =
validate(ngModelController.$modelValue);
});

element.bind('blur', function () {
if (ngModelController.$invalid && ngModelController.$dirty) {
formGroupElement.addClass(valdrClasses.dirtyBlurred);
scope.$on('$destroy', function () {
if (valdrFormGroupController) {
valdrFormGroupController.removeFormItem(ngModelController);
}
});

}
};
}];
Expand Down
Loading

0 comments on commit 3986bf8

Please sign in to comment.