From 3a03d7221965538045153460160612244847c0cc Mon Sep 17 00:00:00 2001 From: Richard Littauer Date: Mon, 21 Apr 2014 17:58:01 -0400 Subject: [PATCH] feat(misc core): add ngId directive Add a way of dynamically updating the id attribute, using either a string or an object. Save the old id automatically in case the string or the object evaluates to falsy. --- angularFiles.js | 1 + src/AngularPublic.js | 2 + src/ng/directive/ngId.js | 135 +++++++++++++++++++++++++++++ test/ng/directive/ngIdSpec.js | 156 ++++++++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 src/ng/directive/ngId.js create mode 100644 test/ng/directive/ngIdSpec.js diff --git a/angularFiles.js b/angularFiles.js index 1647ba48481a..4a5fd9ef29ef 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -53,6 +53,7 @@ angularFiles = { 'src/ng/directive/ngController.js', 'src/ng/directive/ngCsp.js', 'src/ng/directive/ngEventDirs.js', + 'src/ng/directive/ngId.js', 'src/ng/directive/ngIf.js', 'src/ng/directive/ngInclude.js', 'src/ng/directive/ngInit.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index e97723ef946d..3c233a2d874c 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -26,6 +26,7 @@ ngControllerDirective, ngFormDirective, ngHideDirective, + ngIdDirective, ngIfDirective, ngIncludeDirective, ngIncludeFillContentDirective, @@ -166,6 +167,7 @@ function publishExternalAPI(angular){ ngController: ngControllerDirective, ngForm: ngFormDirective, ngHide: ngHideDirective, + ngId: ngIdDirective, ngIf: ngIfDirective, ngInclude: ngIncludeDirective, ngInit: ngInitDirective, diff --git a/src/ng/directive/ngId.js b/src/ng/directive/ngId.js new file mode 100644 index 000000000000..fbe4055290a8 --- /dev/null +++ b/src/ng/directive/ngId.js @@ -0,0 +1,135 @@ +'use strict'; + +function idDirective() { + return function() { + return { + link: function(scope, element, attr) { + var oldVal = attr['id'] ? attr['id'] : null ; + + scope.$watch(attr['ngId'], ngIdWatchAction, true); + + attr.$observe('id', function(value) { + ngIdWatchAction(scope.$eval(attr['ngId'])); + }); + + + function ngIdWatchAction(newVal) { + var newId = typeofId(newVal || []); + if (!newId && !oldVal) { + // Remove id + element.removeAttr('id'); + } else if (!newId && oldVal) { + // Set id attribute to old value + element.attr('id', oldVal); + } else { + // Set id attribute to new value + element.attr('id', newId); + } + } + } + }; + + function typeofId (idVal) { + if (isString(idVal)) { + if (idVal.split(' ').length > 0) { + return idVal.split(' ')[0]; + } + return idVal; + } else if (isObject(idVal)) { + var ids = [], i = 0; + forEach(idVal, function(v, k) { + if (v) { + ids.push(k); + } + }); + return ids[0]; + } + return idVal; + } + }; +} + +/** + * @ngdoc directive + * @name ngId + * @restrict A + * + * @description + * The `ngId` directive allows you to dynamically set Id attributes on an HTML element by databinding + * an expression that represents the id to be added. + * + * The directive operates in two different ways, depending on which of two types the expression + * evaluates to: + * + * 1. If the expression evaluates to a string, the string should be one id. If the string has multiple + * space-delimited ids, the first id is returned. + * + * 2. If the expression evaluates to an object, then the key for the first key-value pair in the object + * to evaluate with a truthy value is used as an id. + * + * If there is already an existing id on that element, it will overwrite that id (if the specified key + * value pair is truthy, or if a valid string is used). When the expression changes, the previous id is + * set as the element attribute again. If there was no original id, and the expression does not evaluate + * as truthy, the id attribute is removed. + * + * If there is already an id or name set elsewhere with the same value, it will apply the id attribute, + * which may result in invalid HTML. + * + * @element ANY + * @param {expression} ngId {@link guide/expression Expression} to eval. The result + * of the evaluation can be a string with a single id name, or a map of id names + * to boolean values. In the case of a map, the name of the first property whose value + * to evaluate as truthy will be added as css id to the element. + * + * @example Example that demonstrates basic bindings via ngClass directive. + + +

Map Syntax Example

+ deleted (apply "strike" id)
+ important (apply "bold" id)
+ error (apply "red" id) +
+

Using String Syntax

+ +
+ + #strike { + text-decoration: line-through; + } + #bold { + font-weight: bold; + } + #red { + color: red; + } + + + var ps = element.all(by.css('p')); + + it('should let you toggle the id', function() { + + expect(ps.first().getAttribute('id')).not.toMatch(/bold/); + expect(ps.first().getAttribute('id')).not.toMatch(/red/); + + element(by.model('important')).click(); + expect(ps.first().getAttribute('id')).toMatch(/bold/); + + element(by.model('error')).click(); + expect(ps.first().getAttribute('id')).toMatch(/bold/); + + element(by.model('important')).click(); + expect(ps.first().getAttribute('id')).toMatch(/red/); + }); + + it('should let you toggle string example', function() { + expect(ps.get(1).getAttribute('id')).toBe(''); + element(by.model('style')).clear(); + element(by.model('style')).sendKeys('red'); + expect(ps.get(1).getAttribute('id')).toBe('red'); + }); + + +
+ */ +var ngIdDirective = idDirective(); + diff --git a/test/ng/directive/ngIdSpec.js b/test/ng/directive/ngIdSpec.js new file mode 100644 index 000000000000..16f967d3cb77 --- /dev/null +++ b/test/ng/directive/ngIdSpec.js @@ -0,0 +1,156 @@ +'use strict'; + +describe('ngId', function() { + var element; + + afterEach(function() { + dealoc(element); + }); + + it('should add new and remove old ids dynamically', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + + expect(element.attr('id') === 'existing').toBeTruthy(); + + $rootScope.dynId = 'A'; + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeFalsy(); + expect(element.attr('id') === 'A').toBeTruthy(); + + $rootScope.dynId = 'B'; + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeFalsy(); + expect(element.attr('id') === 'A').toBeFalsy(); + expect(element.attr('id') === 'B').toBeTruthy(); + + delete $rootScope.dynId; + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeTruthy(); + expect(element.attr('id') === 'A').toBeFalsy(); + expect(element.attr('id') === 'B').toBeFalsy(); + })); + + + it('should support not support ids via an array', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeTruthy(); + expect(element.attr('id') === 'A').toBeFalsy(); + expect(element.attr('id') === 'B').toBeFalsy(); + })); + + + it('should support adding multiple ids conditionally via a map of id names to boolean' + + 'expressions', inject(function($rootScope, $compile) { + var element = $compile( + '
' + + '
')($rootScope); + $rootScope.conditionA = true; + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeFalsy(); + expect(element.attr('id') === 'A').toBeTruthy(); + expect(element.attr('id') === 'B').toBeFalsy(); + expect(element.attr('id') === 'AnotB').toBeFalsy(); + + $rootScope.conditionB = function() { return true; }; + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeFalsy(); + expect(element.attr('id') === 'A').toBeTruthy(); + expect(element.attr('id') === 'B').toBeFalsy(); + expect(element.attr('id') === 'AnotB').toBeFalsy(); + + $rootScope.conditionA = false; + $rootScope.conditionB = function() { return true; }; + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeFalsy(); + expect(element.attr('id') === 'A').toBeFalsy(); + expect(element.attr('id') === 'B').toBeTruthy(); + expect(element.attr('id') === 'AnotB').toBeFalsy(); + })); + + + it('should remove ids when the referenced object is the same but its property is changed', + inject(function($rootScope, $compile) { + var element = $compile('
')($rootScope); + $rootScope.ids = { A: true, B: true }; + $rootScope.$digest(); + expect(element.attr('id') === 'A').toBeTruthy(); + expect(element.attr('id') === 'B').toBeFalsy(); + $rootScope.ids.A = false; + $rootScope.$digest(); + expect(element.attr('id') === 'A').toBeFalsy(); + expect(element.attr('id') === 'B').toBeTruthy(); + } + )); + + + it('should return only the first word in a space delimited string', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeFalsy(); + expect(element.attr('id') === 'A').toBeTruthy(); + expect(element.attr('id') === 'B').toBeFalsy(); + })); + + + it('should replace id added post compilation with pre-existing ng-id value', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynId = 'A'; + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBe(false); + + // add extra id, change model and eval + element.attr('id', 'newId'); + $rootScope.dynId = 'B'; + $rootScope.$digest(); + + expect(element.attr('id') === 'existing').toBe(false); + expect(element.attr('id') === 'B').toBe(true); + expect(element.attr('id') === 'newid').toBe(false); + })); + + + it('should replace id added post compilation without pre-existing ids"', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynId = 'A'; + $rootScope.$digest(); + expect(element.attr('id') === 'A').toBe(true); + + // add extra id, change model and eval + element.attr('id', 'newId'); + $rootScope.dynId= 'B'; + $rootScope.$digest(); + + expect(element.attr('id') === 'B').toBe(true); + expect(element.attr('id') === 'newid').toBe(false); + })); + + it('should remove ids even if it was specified via id attribute', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynId = 'A'; + $rootScope.$digest(); + $rootScope.dynId = 'B'; + $rootScope.$digest(); + expect(element.attr('id') === 'B').toBe(true); + })); + + it('should convert undefined and null values to an empty string', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynId = [undefined, null]; + $rootScope.$digest(); + expect(element[0].id).toBeFalsy(); + })); + + it('should reinstate the original id if the specified id evaluates to false', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.dynId = 'A'; + $rootScope.$digest(); + expect(element.attr('id') === 'A').toBeTruthy(); + + $rootScope.dynId = false; + $rootScope.$digest(); + expect(element.attr('id') === 'existing').toBeTruthy(); + })); + +});