Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

feature(misc core): add ngId directive #7273

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
ngControllerDirective,
ngFormDirective,
ngHideDirective,
ngIdDirective,
ngIfDirective,
ngIncludeDirective,
ngIncludeFillContentDirective,
Expand Down Expand Up @@ -166,6 +167,7 @@ function publishExternalAPI(angular){
ngController: ngControllerDirective,
ngForm: ngFormDirective,
ngHide: ngHideDirective,
ngId: ngIdDirective,
ngIf: ngIfDirective,
ngInclude: ngIncludeDirective,
ngInit: ngInitDirective,
Expand Down
135 changes: 135 additions & 0 deletions src/ng/directive/ngId.js
Original file line number Diff line number Diff line change
@@ -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.
<example>
<file name="index.html">
<p ng-id="{strike: deleted, bold: important, red: error}">Map Syntax Example</p>
<input type="checkbox" ng-model="deleted"> deleted (apply "strike" id)<br>
<input type="checkbox" ng-model="important"> important (apply "bold" id)<br>
<input type="checkbox" ng-model="error"> error (apply "red" id)
<hr>
<p ng-id="style">Using String Syntax</p>
<input type="text" ng-model="style" placeholder="Type: bold strike red">
</file>
<file name="style.css">
#strike {
text-decoration: line-through;
}
#bold {
font-weight: bold;
}
#red {
color: red;
}
</file>
<file name="protractor.js" type="protractor">
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');
});

</file>
</example>
*/
var ngIdDirective = idDirective();

156 changes: 156 additions & 0 deletions test/ng/directive/ngIdSpec.js
Original file line number Diff line number Diff line change
@@ -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('<div id="existing" ng-id="dynId"></div>')($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('<div id="existing" ng-id="[\'A\', \'B\']"></div>')($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(
'<div id="existing" ' +
'ng-id="{A: conditionA, B: conditionB(), AnotB: conditionA&&!conditionB()}">' +
'</div>')($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('<div ng-id="ids"></div>')($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('<div id="existing" ng-id="\'A B\'"></div>')($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('<div id="existing" ng-id="dynId"></div>')($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('<div ng-id="dynId"></div>')($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('<div id="existing" ng-id="dynId"></div>')($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('<div ng-id="dynId"></div>')($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('<div id="existing" ng-id="dynId"></div>')($rootScope);
$rootScope.dynId = 'A';
$rootScope.$digest();
expect(element.attr('id') === 'A').toBeTruthy();

$rootScope.dynId = false;
$rootScope.$digest();
expect(element.attr('id') === 'existing').toBeTruthy();
}));

});