From 4b79321fbd59b4fd7e97ff82257958993185ab01 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Thu, 3 Oct 2013 18:11:29 -0400 Subject: [PATCH] feat(ngAttr): implement conditional/interpolated attributes directive The ngAttr directive allows for attributes to be conditionally applied to an element. It also supports attributes with interpolated values. 1. Simple case: `ng-attr="{checked: isChecked}"` 2. With values: `ng-attr="'data-whatever={{value}}'"` 3. Set of attributes with interpolated values: `ng-attr="['attr1={{a}}', 'attr2={{b()}}']"` 4. Conditional attributes with values: `ng-attr="{'data-whatever=shmee': showWhatever&&!flurp()}"` --- angularFiles.js | 1 + src/AngularPublic.js | 2 + src/ng/directive/ngAttr.js | 76 +++++++++++ test/ng/directive/ngAttrSpec.js | 231 ++++++++++++++++++++++++++++++++ 4 files changed, 310 insertions(+) create mode 100644 src/ng/directive/ngAttr.js create mode 100644 test/ng/directive/ngAttrSpec.js diff --git a/angularFiles.js b/angularFiles.js index ad968341abf9..302b47cfc763 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -45,6 +45,7 @@ angularFiles = { 'src/ng/directive/booleanAttrs.js', 'src/ng/directive/form.js', 'src/ng/directive/input.js', + 'src/ng/directive/ngAttr.js', 'src/ng/directive/ngBind.js', 'src/ng/directive/ngClass.js', 'src/ng/directive/ngCloak.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 14fe25aec20a..7ab83d0c179a 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -15,6 +15,7 @@ selectDirective, styleDirective, optionDirective, + ngAttrDirective, ngBindDirective, ngBindHtmlDirective, ngBindTemplateDirective, @@ -152,6 +153,7 @@ function publishExternalAPI(angular){ select: selectDirective, style: styleDirective, option: optionDirective, + ngAttr: ngAttrDirective, ngBind: ngBindDirective, ngBindHtml: ngBindHtmlDirective, ngBindTemplate: ngBindTemplateDirective, diff --git a/src/ng/directive/ngAttr.js b/src/ng/directive/ngAttr.js new file mode 100644 index 000000000000..e7f08a95cc4e --- /dev/null +++ b/src/ng/directive/ngAttr.js @@ -0,0 +1,76 @@ +'use strict'; + +function attrDirective(name, selector) { + name = 'ngAttr' + name; + return function() { + var ATTR_MATCH = /\s*([^=]+)(=\s*(\S+))?/; + return { + restrict: 'A', + link: function(scope, element, attr) { + var oldVal; + + scope.$watch(attr[name], function(value) { + ngAttrWatchAction(scope.$eval(attr[name])); + }, true); + + attr.$observe(name, function() { + ngAttrWatchAction(scope.$eval(attr[name])); + }); + + function ngAttrWatchAction(newVal) { + if (selector === true || scope.$index % 2 === selector) { + if (oldVal && !equals(newVal,oldVal)) { + attrWorker(oldVal, removeAttr); + } + attrWorker(newVal, setAttr); + } + oldVal = copy(newVal); + } + + + function splitAttr(value) { + var m = ATTR_MATCH.exec(value); + return m && [m[1].replace(/\s+$/, ''), m[3]]; + } + + + function setAttr(value) { + if (value) { + if (value[0] === 'undefined' || value[0] === 'null') { + return; + } + element.attr(value[0], isDefined(value[1]) ? value[1] : ''); + } + } + + function removeAttr(value) { + if (value) { + element.removeAttr(value[0]); + } + } + + function attrWorker(attrVal, action, compare) { + if(isString(attrVal)) { + attrVal = attrVal.split(/\s+/); + } + if(isArray(attrVal)) { + forEach(attrVal, function(v) { + v = splitAttr(v); + action(v); + }); + } else if (isObject(attrVal)) { + var attrs = []; + forEach(attrVal, function(v, k) { + k = splitAttr(k); + if (v) { + action(k); + } + }); + } + } + } + }; + }; +} + +var ngAttrDirective = attrDirective('', true); \ No newline at end of file diff --git a/test/ng/directive/ngAttrSpec.js b/test/ng/directive/ngAttrSpec.js new file mode 100644 index 000000000000..755799029f69 --- /dev/null +++ b/test/ng/directive/ngAttrSpec.js @@ -0,0 +1,231 @@ +'use strict'; + +describe('ngAttr', function() { + var element; + + + afterEach(function() { + dealoc(element); + }); + + + it('should add new and remove old attributes dynamically', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynAttr = 'A'; + $rootScope.$digest(); + expect(element.attr('A')).toBe(''); + expect(element.attr('B')).toBeUndefined(); + + $rootScope.dynAttr = 'B'; + $rootScope.$digest(); + expect(element.attr('existing')).toBe(''); + expect(element.attr('A')).toBeUndefined(); + expect(element.attr('B')).toBe(''); + + delete $rootScope.dynAttr; + $rootScope.$digest(); + expect(element.attr('existing')).toBe(''); + expect(element.attr('A')).toBeUndefined(); + expect(element.attr('B')).toBeUndefined(); + })); + + + it('should support adding multiple attributes via an array', + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.attr('existing')).toBe(''); + expect(element.attr('A')).toBe(''); + expect(element.attr('B')).toBe(''); + })); + + + it('should support adding multiple attributes conditionally via a map of attribute names to' + + ' boolean expressions', inject(function($rootScope, $compile) { + var element = $compile( + '
' + + '
')($rootScope); + $rootScope.conditionA = true; + $rootScope.$digest(); + expect(element.attr('existing')).toBe(''); + expect(element.attr('A')).toBe(''); + expect(element.attr('B')).toBeUndefined(); + expect(element.attr('AnotB')).toBe(''); + + $rootScope.conditionB = function() { return true; }; + $rootScope.$digest(); + expect(element.attr('existing')).toBe(''); + expect(element.attr('A')).toBe(''); + expect(element.attr('B')).toBe(''); + expect (element.attr('AnotB')).toBeUndefined(); + })); + + + it('should support adding multiple attributes with values conditionally via a map of attribute ' + + 'names to boolean expressions', + inject(function($rootScope, $compile) { + var element = $compile( + '
' + + '
')($rootScope); + + $rootScope.conditionA = true; + $rootScope.$digest(); + + $rootScope.a = function() { return "forty-two"; }; + $rootScope.b = "snow-crash"; + + $rootScope.$digest(); + + expect(element.attr('a')).toBe('forty-two'); + expect(element.attr('b')).toBeUndefined(); + + $rootScope.conditionB = function() { return true; }; + $rootScope.$digest(); + expect(element.attr('a')).toBe('forty-two'); + expect(element.attr('b')).toBe('snow-crash'); + })); + + + it('should remove attributes when the referenced object is the same but its property is changed', + inject(function($rootScope, $compile) { + var element = $compile('
')($rootScope); + $rootScope.attributes = { A: true, B: true }; + $rootScope.$digest(); + expect(element.attr('A')).toBe(''); + expect(element.attr('B')).toBe(''); + $rootScope.attributes.A = false; + $rootScope.$digest(); + expect(element.attr('A')).toBeUndefined(); + expect(element.attr('B')).toBe(''); + })); + + + it('should support adding attributes with values', inject(function($rootScope, $compile) { + var element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.attr('value')).toBe('seventy'); + })); + + + it('should support adding multiple attributes via a space delimited string', + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.attr('existing')).toBe(''); + expect(element.attr('A')).toBe(''); + expect(element.attr('B')).toBe(''); + })); + + + it('should support adding multiple attributes with values via array notation', + inject(function($rootScope, $compile) { + var element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.attr('value')).toBe('seventy'); + expect(element.attr('value2')).toBe('spork'); + })); + + + it('should preserve attribute added post compilation with pre-existing attributes', + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynAttr = 'A'; + $rootScope.$digest(); + expect(element.attr('existing')).toBe(''); + + // add extra attribute, change model and eval + element.attr('newAttr',''); + $rootScope.dynAttr = 'B'; + $rootScope.$digest(); + + expect(element.attr('existing')).toBe(''); + expect(element.attr('B')).toBe(''); + expect(element.attr('newAttr')).toBe(''); + })); + + + it('should preserve attribute added post compilation without pre-existing attributes"', + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynAttr = 'A'; + $rootScope.$digest(); + expect(element.attr('A')).toBe(''); + + // add extra attribute, change model and eval + element.attr('newAttr',''); + $rootScope.dynAttr = 'B'; + $rootScope.$digest(); + + expect(element.attr('B')).toBe(''); + expect(element.attr('newAttr')).toBe(''); + })); + + + it('should preserve other attributes with similar name"', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynAttr = 'panel'; + $rootScope.$digest(); + $rootScope.dynAttr = 'foo'; + $rootScope.$digest(); + expect(element.attr('ui-panel')).toBe(''); + expect(element.attr('ui-selected')).toBe(''); + expect(element.attr('foo')).toBe(''); + })); + + + it('should not add duplicate attributes', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynAttr = 'panel'; + $rootScope.$digest(); + expect(element.attr('panel')).toBe(''); + expect(element.attr('bar')).toBe(''); + })); + + + it('should remove attributes even if it was specified outside ng-attr', + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynAttr = 'panel'; + $rootScope.$digest(); + $rootScope.dynAttr = 'window'; + $rootScope.$digest(); + expect(element.attr('bar')).toBe(''); + expect(element.attr('window')).toBe(''); + })); + + + it('should remove attributes even if they were added by another code', + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynAttr = 'foo'; + $rootScope.$digest(); + element.attr('foo', ''); + $rootScope.dynAttr = ''; + $rootScope.$digest(); + expect(element.attr('foo')).toBeUndefined(); + })); + + + it('should not add attributes with empty names', + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynAttr = [undefined, null]; + $rootScope.$digest(); + expect(element.attr('undefined')).toBeUndefined(); + expect(element.attr('null')).toBeUndefined(); + })); + + + it('should not mess up attribute value due to observing an interpolated attribute', + inject(function($rootScope, $compile) { + $rootScope.foo = true; + $rootScope.$watch("anything", function() { + $rootScope.foo = false; + }); + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.attr('foo')).toBeUndefined(); + })); +});