Skip to content

Refactoring #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
237 changes: 162 additions & 75 deletions client/src/js/ng-django-forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,57 +146,118 @@ djng_forms_module.directive('ngModel', function() {
});


// This directive is added automatically by django-angular for widgets of type RadioSelect and
// CheckboxSelectMultiple. This is necessary to adjust the behavior of a collection of input fields,
// which forms a group for one `django.forms.Field`.
// This directive is added automatically by django-angular for widgets of type CheckboxSelectMultiple.
// This is necessary to adjust the behavior of a collection of input fields, which forms a group for
// one `django.forms.Field`.
djng_forms_module.directive('validateMultipleFields', function() {
return {
restrict: 'A',
require: '^?form',
link: function(scope, element, attrs, formCtrl) {
var subFields, checkboxElems = [];

function validate(event) {
var valid = false;
angular.forEach(checkboxElems, function(checkbox) {
valid = valid || checkbox.checked;
});
formCtrl.$setValidity('required', valid);
if (event) {
formCtrl.$dirty = true;
formCtrl.$pristine = false;
// element.on('change', validate) is jQuery and runs outside of Angular's digest cycle.
// Therefore Angular does not get the end-of-digest signal and $apply() must be invoked manually.
scope.$apply();
require: 'validateMultipleFields',
controller: 'ValidateMultipleFieldsCtrl',
link: {
pre: function(scope, element, attrs, ctrl) {

var subFields;

try {
subFields = angular.fromJson(attrs.validateMultipleFields);
} catch (SyntaxError) {
if (!angular.isString(attrs.validateMultipleFields))
return;
subFields = attrs.validateMultipleFields;
}
}

if (!formCtrl)
return;
try {
subFields = angular.fromJson(attrs.validateMultipleFields);
} catch (SyntaxError) {
if (!angular.isString(attrs.validateMultipleFields))
return;
subFields = [attrs.validateMultipleFields];
formCtrl = formCtrl[subFields];
ctrl.setSubFields(subFields);
},
post: function(scope, element, attrs, ctrl) {
ctrl.controlStateChange();
}
angular.forEach(element.find('input'), function(elem) {
if (subFields.indexOf(elem.name) >= 0) {
checkboxElems.push(elem);
angular.element(elem).on('change', validate);
}
});
}
}
});


// remove "change" event handlers from each input field
element.on('$destroy', function() {
angular.forEach(element.find('input'), function(elem) {
angular.element(elem).off('change');
djng_forms_module.controller('ValidateMultipleFieldsCtrl', function() {

var vm = this,
ctrls,
subFields;

vm.setSubFields = setSubFields;
vm.registerCtrl = registerCtrl;
vm.controlStateChange = controlStateChange;

/* ------------- */

function setSubFields(value) {
subFields = value;
}

function registerCtrl(ctrl) {

if(_isNotValidSubField(ctrl.$name))
return;

ctrls = ctrls || [];
ctrls.push(ctrl);

return true;
}

function controlStateChange() {

var value = false;

// get collective value for group
angular.forEach(ctrls, function(ctrl) {
value = !!(value || ctrl.$modelValue);
});

/*
* set 'required' validity of all controls depending on value.
* this then automatically sets the 'required' error state of the parent ngForm
*/
angular.forEach(ctrls, function(ctrl) {
ctrl.$setValidity('required', value);
if(ctrl.djngClearRejected)
ctrl.djngClearRejected();
});
}

function _isNotValidSubField(name) {
return !!subFields && subFields.indexOf(name) == -1;
}
});


djng_forms_module.directive('ngModel', function() {
return {
restrict:'A',
/*
* ensure that this gets fired after ng.django.forms restore value ngModel
* directive, as if initial/bound value is set, $viewChangeListener is fired
*/
priority: 2,
require: [
'?^validateMultipleFields',
'?ngModel'
],
link: function(scope, element, attrs, ctrls) {

var vmfCtrl = ctrls[0],
ngModel = ctrls[1];

if(!vmfCtrl || !ngModel)
return;

if(vmfCtrl.registerCtrl(ngModel)) {

ngModel.$viewChangeListeners.push(function() {
vmfCtrl.controlStateChange();
});
});
validate();
}
}
};
}
});


Expand Down Expand Up @@ -245,6 +306,41 @@ djng_forms_module.directive('validateDate', function() {
});


djng_forms_module.directive('djngRejected', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ctrl) {

if(!ctrl || attrs.djngRejected !== '')
return;

var clearRejectedError = function(value) {

if(ctrl.$error.rejected)
ctrl.djngClearRejected();

return value;
};

ctrl.djngClearRejected = function() {
ctrl.$message = undefined;
ctrl.$setValidity('rejected', true);
};

ctrl.djngAddRejected = function(msg) {
ctrl.$message = msg;
ctrl.$setValidity('rejected', false);
ctrl.$setPristine();
};

ctrl.$formatters.push(clearRejectedError);
ctrl.$parsers.push(clearRejectedError);
}
}
});


// If forms are validated using Ajax, the server shall return a dictionary of detected errors to the
// client code. The success-handler of this Ajax call, now can set those error messages on their
// prepared list-items. The simplest way, is to add this code snippet into the controllers function
Expand All @@ -263,18 +359,9 @@ djng_forms_module.factory('djangoForm', function() {
}
return false;
}

function resetFieldValidity(field) {
var pos = field.$viewChangeListeners.push(field.clearRejected = function() {
field.$message = '';
field.$setValidity('rejected', true);
field.$viewChangeListeners.splice(pos - 1, 1);
delete field.clearRejected;
});
}

function isField(field) {
return angular.isArray(field.$viewChangeListeners);
return !!field && angular.isArray(field.$viewChangeListeners);
}

return {
Expand All @@ -295,14 +382,14 @@ djng_forms_module.factory('djangoForm', function() {
var field, key = rejected.$name;
if (form.hasOwnProperty(key)) {
field = form[key];
if (isField(field) && field.clearRejected) {
field.clearRejected();
if (isField(field) && field.djngClearRejected) {
field.djngClearRejected();
} else {
field.$message = '';
// this field is a composite of input elements
angular.forEach(field, function(subField, subKey) {
if (subField && isField(subField) && subField.clearRejected) {
subField.clearRejected();
if (isField(subField) && subField.djngClearRejected) {
subField.djngClearRejected();
}
});
}
Expand All @@ -312,25 +399,25 @@ djng_forms_module.factory('djangoForm', function() {
// add the new upstream errors
angular.forEach(errors, function(errors, key) {
var field;
if (errors.length > 0) {
if (key === NON_FIELD_ERRORS) {
form.$message = errors[0];
form.$setPristine();
} else if (form.hasOwnProperty(key)) {
field = form[key];
field.$message = errors[0];
field.$setValidity('rejected', false);
field.$setPristine();
if (isField(field)) {
resetFieldValidity(field);
} else {
// this field is a composite of input elements
angular.forEach(field, function(subField, subKey) {
if (subField && isField(subField)) {
resetFieldValidity(subField);
}
});
}
if (errors.length === 0)
return;
if (key === NON_FIELD_ERRORS) {
form.$message = errors[0];
form.$setPristine();
} else if (form.hasOwnProperty(key)) {
field = form[key];
if (isField(field)) {
field.djngAddRejected(errors[0]);
} else {
// this field is a composite of input elements
angular.forEach(field, function(subField, subKey) {
if (isField(subField)) {
// add message to ngForm
field.$message = errors[0];
field.$setPristine();
subField.djngAddRejected(errors[0]);
}
});
}
}
});
Expand Down
66 changes: 64 additions & 2 deletions client/tests/djangoFormsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe('unit tests for module ng.django.forms', function() {
function compileForm($compile, scope, replace_value) {
var template =
'<form name="valid_form" action=".">' +
'<input name="email_field" ng-model="model.email" type="text" {value} />' +
'<input name="email_field" ng-model="model.email" djng-rejected type="text" {value} />' +
'</form>';
var form = angular.element(template.replace('{value}', replace_value));
$compile(form)(scope);
Expand Down Expand Up @@ -60,6 +60,68 @@ describe('unit tests for module ng.django.forms', function() {
});

});

describe('test directive djngRejected', function() {

var scope, form, field;

beforeEach(function() {
module('ng.django.forms');
});

beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
compileForm($compile, scope, '');
form = scope.valid_form;
field = scope.valid_form.email_field;
}));

it('should add djngAddRejected method to ngModel', function() {
expect(typeof field.djngAddRejected).toBe('function');
});

it('should add djngClearRejected method to ngModel', function() {
expect(typeof field.djngClearRejected).toBe('function');
});

it('should set rejected state on control', function() {
field.djngAddRejected('i am rejected');
expect(field.$error.rejected).toBe(true);
expect(field.$message).toBe('i am rejected');
expect(field.$pristine).toBe(true);
});

it('should remove rejected state from control', function() {
field.djngAddRejected('i am rejected');
expect(field.$error.rejected).toBe(true);
expect(field.$message).toBe('i am rejected');
expect(field.$pristine).toBe(true);
field.djngClearRejected();
expect(field.$error.rejected).toBe(false);
expect(field.$message).toBe(undefined);
});

it('should remove rejected state when model changes', function() {
field.djngAddRejected('i am rejected');
expect(field.$error.rejected).toBe(true);
expect(field.$message).toBe('i am rejected');
expect(field.$pristine).toBe(true);
scope.model = {email: '[email protected]'};
scope.$digest();
expect(field.$error.rejected).toBe(false);
expect(field.$message).toBe(undefined);
});

it('should remove rejected state when $viewValue changes', function() {
field.djngAddRejected('i am rejected');
expect(field.$error.rejected).toBe(true);
expect(field.$message).toBe('i am rejected');
expect(field.$pristine).toBe(true);
field.$setViewValue('[email protected]');
expect(field.$error.rejected).toBe(false);
expect(field.$message).toBe(undefined);
});
});

describe('test directive validateDate', function() {
var scope, form;
Expand Down Expand Up @@ -125,7 +187,7 @@ describe('unit tests for module ng.django.forms', function() {
beforeEach(inject(function($compile) {
var form = angular.element(
'<form name="form" action=".">' +
'<input name="email_field" ng-model="model.email" type="text" />' +
'<input name="email_field" ng-model="model.email" djng-rejected type="text" />' +
'</form>'
);
$compile(form)(scope);
Expand Down
6 changes: 4 additions & 2 deletions djangular/forms/angular_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,18 @@ def get_field_errors(self, field):
if field.is_hidden:
return errors
identifier = format_html('{0}.{1}', self.form_name, field.html_name)
errors.append(SafeTuple((identifier, self.field_error_css_classes, '$pristine', '$message', 'invalid', '$message')))
errors.append(SafeTuple((identifier, self.field_error_css_classes, '$pristine', '$error.rejected', 'invalid', '$message')))
return errors

def non_field_errors(self):
errors = super(NgModelFormMixin, self).non_field_errors()
errors.append(SafeTuple((self.form_name, self.form_error_css_classes, '$pristine', '$message', 'invalid', '$message')))
errors.append(SafeTuple((self.form_name, self.form_error_css_classes, '$pristine', '$error.rejected', 'invalid', '$message')))
return errors

def get_widget_attrs(self, bound_field):
attrs = super(NgModelFormMixin, self).get_widget_attrs(bound_field)
# add rejected error removal directive
attrs.update({'djng-rejected': ''})
identifier = self.add_prefix(bound_field.name)
ng = {
'name': bound_field.name,
Expand Down
Loading