From 2bb5fcfb362e4fd20c27ff650214009c92e8f67b Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Mon, 30 Mar 2015 22:16:13 +0100 Subject: [PATCH 1/8] removed validate-multiple-fields for RadioSelect. Added ngForm wrapper. Added required attribute to each radio button --- djangular/forms/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangular/forms/widgets.py b/djangular/forms/widgets.py index 01e1b1b..022cb62 100644 --- a/djangular/forms/widgets.py +++ b/djangular/forms/widgets.py @@ -81,9 +81,9 @@ def get_field_attrs(self, field): class RadioFieldRendererMixin(object): def __init__(self, name, value, attrs, choices): attrs.pop('djng-error', None) - self.field_attrs = [] + self.field_attrs = [format_html('ng-form="{0}"', name)] if attrs.pop('radio_select_required', False): - self.field_attrs.append(format_html('validate-multiple-fields="{0}"', name)) + attrs.update({'required': ''}) super(RadioFieldRendererMixin, self).__init__(name, value, attrs, choices) From 8313ddba6e5200ceb496dd6a7f9cfd9e7dc44fbf Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Mon, 30 Mar 2015 22:52:37 +0100 Subject: [PATCH 2/8] rejected state now set on each child ngModel instead of ngForm --- client/src/js/ng-django-forms.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index 48d1e2d..52a46ed 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -319,14 +319,16 @@ djng_forms_module.factory('djangoForm', function() { } else if (form.hasOwnProperty(key)) { field = form[key]; field.$message = errors[0]; - field.$setValidity('rejected', false); field.$setPristine(); if (isField(field)) { + field.$setValidity('rejected', false); resetFieldValidity(field); } else { // this field is a composite of input elements angular.forEach(field, function(subField, subKey) { if (subField && isField(subField)) { + subField.$setValidity('rejected', false); + subField.$message = errors[0]; resetFieldValidity(subField); } }); From 89ca7d8255bc709de6cc259a10e8f6ac8d093307 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Mon, 30 Mar 2015 23:44:10 +0100 Subject: [PATCH 3/8] added djng-rejected directive to handle rejected error removal. Refactored djangoForms.setErrors to work with this --- client/src/js/ng-django-forms.js | 44 +++++++++++++++++++++++--------- djangular/forms/angular_model.py | 6 +++-- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index 52a46ed..07b4bca 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -245,6 +245,32 @@ 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) + return; + + var validator = function(value) { + + if(ctrl.$error.rejected) { + ctrl.$message = undefined; + ctrl.$setValidity('rejected', true); + } + + return value; + }; + + ctrl.$formatters.push(validator); + ctrl.$parsers.push(validator); + } + } +}); + + // 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 @@ -264,13 +290,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 clearRejected(field) { + field.$message = ''; + field.$setValidity('rejected', true); } function isField(field) { @@ -295,14 +317,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)) { + clearRejected(field); } 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(); + clearRejected(subField); } }); } @@ -322,14 +344,12 @@ djng_forms_module.factory('djangoForm', function() { field.$setPristine(); if (isField(field)) { field.$setValidity('rejected', false); - resetFieldValidity(field); } else { // this field is a composite of input elements angular.forEach(field, function(subField, subKey) { if (subField && isField(subField)) { subField.$setValidity('rejected', false); subField.$message = errors[0]; - resetFieldValidity(subField); } }); } diff --git a/djangular/forms/angular_model.py b/djangular/forms/angular_model.py index 595c174..99005b4 100644 --- a/djangular/forms/angular_model.py +++ b/djangular/forms/angular_model.py @@ -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, From 2864f24ecefbaca52e48ad5f28c110e8857bf4a0 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Tue, 31 Mar 2015 00:23:23 +0100 Subject: [PATCH 4/8] moved rejected error add and remove onto ngModel --- client/src/js/ng-django-forms.js | 36 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index 07b4bca..724b02e 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -263,6 +263,17 @@ djng_forms_module.directive('djngRejected', function() { 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(validator); ctrl.$parsers.push(validator); @@ -289,11 +300,6 @@ djng_forms_module.factory('djangoForm', function() { } return false; } - - function clearRejected(field) { - field.$message = ''; - field.$setValidity('rejected', true); - } function isField(field) { return angular.isArray(field.$viewChangeListeners); @@ -317,14 +323,14 @@ djng_forms_module.factory('djangoForm', function() { var field, key = rejected.$name; if (form.hasOwnProperty(key)) { field = form[key]; - if (isField(field)) { - clearRejected(field); + 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) { - clearRejected(subField); + if (subField && isField(subField) && subField.djngClearRejected) { + subField.djngClearRejected(); } }); } @@ -340,16 +346,18 @@ djng_forms_module.factory('djangoForm', function() { form.$setPristine(); } else if (form.hasOwnProperty(key)) { field = form[key]; - field.$message = errors[0]; - field.$setPristine(); + if (isField(field)) { - field.$setValidity('rejected', false); + field.djngAddRejected(errors[0]); } else { // this field is a composite of input elements angular.forEach(field, function(subField, subKey) { if (subField && isField(subField)) { - subField.$setValidity('rejected', false); - subField.$message = errors[0]; + + field.$message = errors[0]; + field.$setPristine(); + + subField.djngAddRejected(errors[0]); } }); } From 3d1d87d21d78e9fe8a8c7ea30358a3c48f8592a5 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Tue, 31 Mar 2015 12:01:41 +0100 Subject: [PATCH 5/8] moved add and removed rejected error message on to ngModel. Updated djangoForms.setErrors and validate-multiple-fields to handle this --- client/src/js/ng-django-forms.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index 724b02e..4fe0979 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -157,9 +157,13 @@ djng_forms_module.directive('validateMultipleFields', function() { var subFields, checkboxElems = []; function validate(event) { - var valid = false; + var valid = false, + ctrl; angular.forEach(checkboxElems, function(checkbox) { valid = valid || checkbox.checked; + ctrl = formCtrl[checkbox.name]; + if(ctrl && ctrl.djngClearRejected) + ctrl.djngClearRejected(); }); formCtrl.$setValidity('required', valid); if (event) { @@ -251,15 +255,13 @@ djng_forms_module.directive('djngRejected', function() { require: '?ngModel', link: function(scope, element, attrs, ctrl) { - if(!ctrl) + if(!ctrl || attrs.djngRejected !== '') return; - + var validator = function(value) { if(ctrl.$error.rejected) { - ctrl.$message = undefined; - ctrl.$setValidity('rejected', true); - } + ctrl.djngClearRejected(); return value; }; From d930c09ddd0e7a2a9b874836fa58ca6c90fd7323 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Tue, 31 Mar 2015 12:38:31 +0100 Subject: [PATCH 6/8] fixed failing tests. Added client tests for djngRejected directive --- client/src/js/ng-django-forms.js | 2 +- client/tests/djangoFormsSpec.js | 66 +++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index 4fe0979..0fbe41d 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -260,7 +260,7 @@ djng_forms_module.directive('djngRejected', function() { var validator = function(value) { - if(ctrl.$error.rejected) { + if(ctrl.$error.rejected) ctrl.djngClearRejected(); return value; diff --git a/client/tests/djangoFormsSpec.js b/client/tests/djangoFormsSpec.js index 3285901..686c3c7 100644 --- a/client/tests/djangoFormsSpec.js +++ b/client/tests/djangoFormsSpec.js @@ -4,7 +4,7 @@ describe('unit tests for module ng.django.forms', function() { function compileForm($compile, scope, replace_value) { var template = '
' + - '' + + '' + '
'; var form = angular.element(template.replace('{value}', replace_value)); $compile(form)(scope); @@ -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: 'barry@barry.com'}; + 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('barry@barry.com'); + expect(field.$error.rejected).toBe(false); + expect(field.$message).toBe(undefined); + }); + }); describe('test directive validateDate', function() { var scope, form; @@ -125,7 +187,7 @@ describe('unit tests for module ng.django.forms', function() { beforeEach(inject(function($compile) { var form = angular.element( '
' + - '' + + '' + '
' ); $compile(form)(scope); From 919d6e0507dbf3b986c24eb5fd1a1cf9befdc7d7 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Tue, 31 Mar 2015 16:39:40 +0100 Subject: [PATCH 7/8] refactored validate-multiple-fields directive to reflect the other updates --- client/src/js/ng-django-forms.js | 189 ++++++++++++++++++++----------- 1 file changed, 122 insertions(+), 67 deletions(-) diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index 0fbe41d..517aa47 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -146,61 +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, - ctrl; - angular.forEach(checkboxElems, function(checkbox) { - valid = valid || checkbox.checked; - ctrl = formCtrl[checkbox.name]; - if(ctrl && ctrl.djngClearRejected) - ctrl.djngClearRejected(); - }); - 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); - } - }); + } + } +}); + + +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; + } +}); + - // 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.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(); + } } - }; + } }); @@ -342,27 +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]; - - if (isField(field)) { - field.djngAddRejected(errors[0]); - } else { - // this field is a composite of input elements - angular.forEach(field, function(subField, subKey) { - if (subField && isField(subField)) { - - field.$message = errors[0]; - field.$setPristine(); - - subField.djngAddRejected(errors[0]); - } - }); - } + 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 (subField && isField(subField)) { + // add message to ngForm + field.$message = errors[0]; + field.$setPristine(); + subField.djngAddRejected(errors[0]); + } + }); } } }); From 926b289d4bdf0edb8018e09a439440faf5384d5f Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Wed, 1 Apr 2015 19:55:59 +0100 Subject: [PATCH 8/8] tweak --- client/src/js/ng-django-forms.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index 517aa47..0d3acaf 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -315,7 +315,7 @@ djng_forms_module.directive('djngRejected', function() { if(!ctrl || attrs.djngRejected !== '') return; - var validator = function(value) { + var clearRejectedError = function(value) { if(ctrl.$error.rejected) ctrl.djngClearRejected(); @@ -334,8 +334,8 @@ djng_forms_module.directive('djngRejected', function() { ctrl.$setPristine(); }; - ctrl.$formatters.push(validator); - ctrl.$parsers.push(validator); + ctrl.$formatters.push(clearRejectedError); + ctrl.$parsers.push(clearRejectedError); } } }); @@ -361,7 +361,7 @@ djng_forms_module.factory('djangoForm', function() { } function isField(field) { - return angular.isArray(field.$viewChangeListeners); + return !!field && angular.isArray(field.$viewChangeListeners); } return { @@ -388,7 +388,7 @@ djng_forms_module.factory('djangoForm', function() { field.$message = ''; // this field is a composite of input elements angular.forEach(field, function(subField, subKey) { - if (subField && isField(subField) && subField.djngClearRejected) { + if (isField(subField) && subField.djngClearRejected) { subField.djngClearRejected(); } }); @@ -411,7 +411,7 @@ djng_forms_module.factory('djangoForm', function() { } else { // this field is a composite of input elements angular.forEach(field, function(subField, subKey) { - if (subField && isField(subField)) { + if (isField(subField)) { // add message to ngForm field.$message = errors[0]; field.$setPristine();