diff --git a/lib/directive/module.dart b/lib/directive/module.dart index 2988449af..b9978215c 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -18,6 +18,7 @@ part 'ng_events.dart'; part 'ng_cloak.dart'; part 'ng_if.dart'; part 'ng_include.dart'; +part 'ng_control.dart'; part 'ng_model.dart'; part 'ng_pluralize.dart'; part 'ng_repeat.dart'; diff --git a/lib/directive/ng_control.dart b/lib/directive/ng_control.dart new file mode 100644 index 000000000..47cfc208b --- /dev/null +++ b/lib/directive/ng_control.dart @@ -0,0 +1,56 @@ +part of angular.directive; + +abstract class NgControl { + static const NG_VALID_CLASS = "ng-valid"; + static const NG_INVALID_CLASS = "ng-invalid"; + static const NG_PRISTINE_CLASS = "ng-pristine"; + static const NG_DIRTY_CLASS = "ng-dirty"; + + String _name; + bool _dirty; + bool _pristine; + bool _valid; + bool _invalid; + + get element => null; + + get name => _name; + set name(name) => _name = name; + + get pristine => _pristine; + set pristine(value) { + _pristine = true; + _dirty = false; + + element.classes.remove(NG_DIRTY_CLASS); + element.classes.add(NG_PRISTINE_CLASS); + } + + get dirty => _dirty; + set dirty(value) { + _dirty = true; + _pristine = false; + + element.classes.remove(NG_PRISTINE_CLASS); + element.classes.add(NG_DIRTY_CLASS); + } + + get valid => _valid; + set valid(value) { + _invalid = false; + _valid = true; + + element.classes.remove(NG_INVALID_CLASS); + element.classes.add(NG_VALID_CLASS); + } + + get invalid => _invalid; + set invalid(value) { + _valid = false; + _invalid = true; + + element.classes.remove(NG_VALID_CLASS); + element.classes.add(NG_INVALID_CLASS); + } + +} diff --git a/lib/directive/ng_form.dart b/lib/directive/ng_form.dart index d9b8bd8c5..d0b0a2b1b 100644 --- a/lib/directive/ng_form.dart +++ b/lib/directive/ng_form.dart @@ -14,24 +14,14 @@ part of angular.directive; @NgDirective( selector: '[ng-form]', visibility: NgDirective.CHILDREN_VISIBILITY) -class NgForm extends NgDetachAware { - static const String NG_VALID_CLASS = "ng-valid"; - static const String NG_INVALID_CLASS = "ng-invalid"; - static const String NG_PRISTINE_CLASS = "ng-pristine"; - static const String NG_DIRTY_CLASS = "ng-dirty"; - +class NgForm extends NgControl implements NgDetachAware { final dom.Element _element; final Scope _scope; - String _name; - - bool _dirty; - bool _pristine; - bool _valid; - bool _invalid; + final Map> currentErrors = new Map>(); - final List _controls = new List(); - final Map _controlByName = new Map(); + final List _controls = new List(); + final Map _controlByName = new Map(); NgForm(this._scope, this._element) { if(!this._element.attributes.containsKey('action')) { @@ -49,6 +39,8 @@ class NgForm extends NgDetachAware { } } + get element => _element; + @NgAttr('name') get name => _name; set name(name) { @@ -56,54 +48,44 @@ class NgForm extends NgDetachAware { _scope[name] = this; } - get pristine => _pristine; - set pristine(value) { - _pristine = true; - _dirty = false; - - _element.classes.remove(NG_DIRTY_CLASS); - _element.classes.add(NG_PRISTINE_CLASS); - } - - get dirty => _dirty; - set dirty(value) { - _dirty = true; - _pristine = false; - - _element.classes.remove(NG_PRISTINE_CLASS); - _element.classes.add(NG_DIRTY_CLASS); - } - - get valid => _valid; - set valid(value) { - _invalid = false; - _valid = true; - - _element.classes.remove(NG_INVALID_CLASS); - _element.classes.add(NG_VALID_CLASS); - } - - get invalid => _invalid; - set invalid(value) { - _valid = false; - _invalid = true; - - _element.classes.remove(NG_VALID_CLASS); - _element.classes.add(NG_INVALID_CLASS); + setValidity(NgControl control, String errorType, bool isValid) { + List queue = currentErrors[errorType]; + + if(isValid) { + if(queue != null) { + queue.remove(control); + if(queue.isEmpty) { + currentErrors.remove(errorType); + if(currentErrors.isEmpty) { + valid = true; + } + } + } + } else { + if(queue == null) { + queue = new List(); + currentErrors[errorType] = queue; + } else if(queue.contains(control)) { + return; + } + + queue.add(control); + invalid = true; + } } operator[](name) { return _controlByName[name]; } - addControl(NgModel control) { + addControl(NgControl control) { _controls.add(control); if(control.name != null) { _controlByName[control.name] = control; } } - removeControl(NgModel control) { + removeControl(NgControl control) { _controls.remove(control); if(control.name != null) { _controlByName.remove(control.name); diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart index 1104dcf57..eb640f0b9 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -12,7 +12,7 @@ part of angular.directive; */ @NgDirective( selector: '[ng-model]') -class NgModel { +class NgModel extends NgControl { final NgForm _form; final dom.Element _element; final Scope _scope; @@ -23,10 +23,7 @@ class NgModel { String _exp; String _name; - bool _dirty; - bool _pristine; - bool _valid; - bool _invalid; + final Map currentErrors = new Map(); Function _removeWatch = () => null; bool _watchCollection; @@ -41,6 +38,8 @@ class NgModel { pristine = true; } + get element => _element; + @NgAttr('name') get name => _name; set name(value) { @@ -74,39 +73,24 @@ class NgModel { get modelValue => getter(); set modelValue(value) => setter(value); - get pristine => _pristine; - set pristine(value) { - _pristine = true; - _dirty = false; - _element.classes.remove(NgForm.NG_DIRTY_CLASS); - _element.classes.add(NgForm.NG_PRISTINE_CLASS); - } - - get dirty => _dirty; - set dirty(value) { - _dirty = true; - _pristine = false; - _element.classes.remove(NgForm.NG_PRISTINE_CLASS); - _element.classes.add(NgForm.NG_DIRTY_CLASS); - } - - get valid => _valid; - set valid(value) { - _invalid = false; - _valid = true; - _element.classes.remove(NgForm.NG_INVALID_CLASS); - _element.classes.add(NgForm.NG_VALID_CLASS); - } + setValidity(String errorType, bool isValid) { + if(isValid) { + if(currentErrors.containsKey(errorType)) { + currentErrors.remove(errorType); + if(currentErrors.isEmpty) { + valid = true; + } + } + } else if(!currentErrors.containsKey(errorType)) { + currentErrors[errorType] = true; + invalid = true; + } - get invalid => _invalid; - set invalid(value) { - _valid = false; - _invalid = true; - _element.classes.remove(NgForm.NG_VALID_CLASS); - _element.classes.add(NgForm.NG_INVALID_CLASS); + if(_form != null) { + _form.setValidity(this, errorType, isValid); + } } - destroy() { _form.removeControl(this); } diff --git a/test/directive/ng_form_spec.dart b/test/directive/ng_form_spec.dart index 8be339e50..4365f74d2 100644 --- a/test/directive/ng_form_spec.dart +++ b/test/directive/ng_form_spec.dart @@ -77,6 +77,92 @@ describe('form', () { expect(element.hasClass('ng-invalid')).toBe(false); expect(element.hasClass('ng-valid')).toBe(true); })); + + it('should set the validity with respect to all existing validations when setValidity() is used', inject((Scope scope) { + var element = $('
' + + ' ' + + ' ' + + ' ' + + '
'); + + _.compile(element); + scope.$apply(); + + var form = scope['myForm']; + NgModel one = form['one']; + NgModel two = form['two']; + NgModel three = form['three']; + + form.setValidity(one, "some error", false); + expect(form.valid).toBe(false); + expect(form.invalid).toBe(true); + + form.setValidity(two, "some error", false); + expect(form.valid).toBe(false); + expect(form.invalid).toBe(true); + + form.setValidity(one, "some error", true); + expect(form.valid).toBe(false); + expect(form.invalid).toBe(true); + + form.setValidity(two, "some error", true); + expect(form.valid).toBe(true); + expect(form.invalid).toBe(false); + })); + + it('should not handle the control + errorType pair more than once', inject((Scope scope) { + var element = $('
' + + ' ' + + '
'); + + _.compile(element); + scope.$apply(); + + var form = scope['myForm']; + NgModel one = form['one']; + + form.setValidity(one, "validation error", false); + expect(form.valid).toBe(false); + expect(form.invalid).toBe(true); + + form.setValidity(one, "validation error", false); + expect(form.valid).toBe(false); + expect(form.invalid).toBe(true); + + form.setValidity(one, "validation error", true); + expect(form.valid).toBe(true); + expect(form.invalid).toBe(false); + })); + + it('should update the validity of the parent form when the inner model changes', inject((Scope scope) { + var element = $('
' + + ' ' + + ' ' + + '
'); + + _.compile(element); + scope.$apply(); + + var form = scope['myForm']; + NgModel one = form['one']; + NgModel two = form['two']; + + one.setValidity("required", false); + expect(form.valid).toBe(false); + expect(form.invalid).toBe(true); + + two.setValidity("required", false); + expect(form.valid).toBe(false); + expect(form.invalid).toBe(true); + + one.setValidity("required", true); + expect(form.valid).toBe(false); + expect(form.invalid).toBe(true); + + two.setValidity("required", true); + expect(form.valid).toBe(true); + expect(form.invalid).toBe(false); + })); }); describe('controls', () { diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart index 2657b1862..34f02c87f 100644 --- a/test/directive/ng_model_spec.dart +++ b/test/directive/ng_model_spec.dart @@ -513,6 +513,46 @@ describe('ng-model', () { expect(element.classes.contains('ng-invalid')).toBe(false); expect(element.classes.contains('ng-valid')).toBe(true); })); + + it('should set the validity with respect to all existing validations when setValidity() is used', inject((Scope scope) { + _.compile(''); + Probe probe = _.rootScope.i; + var model = probe.directive(NgModel); + + model.setValidity("required", false); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + + model.setValidity("format", false); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + + model.setValidity("format", true); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + + model.setValidity("required", true); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + })); + + it('should register each error only once when invalid', inject((Scope scope) { + _.compile(''); + Probe probe = _.rootScope.i; + var model = probe.directive(NgModel); + + model.setValidity("distinct-error", false); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + + model.setValidity("distinct-error", false); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + + model.setValidity("distinct-error", true); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + })); }); });