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

feat(ngAttr): implement conditional/interpolated attributes directive #4269

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 @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
selectDirective,
styleDirective,
optionDirective,
ngAttrDirective,
ngBindDirective,
ngBindHtmlDirective,
ngBindTemplateDirective,
Expand Down Expand Up @@ -152,6 +153,7 @@ function publishExternalAPI(angular){
select: selectDirective,
style: styleDirective,
option: optionDirective,
ngAttr: ngAttrDirective,
ngBind: ngBindDirective,
ngBindHtml: ngBindHtmlDirective,
ngBindTemplate: ngBindTemplateDirective,
Expand Down
76 changes: 76 additions & 0 deletions src/ng/directive/ngAttr.js
Original file line number Diff line number Diff line change
@@ -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);
231 changes: 231 additions & 0 deletions test/ng/directive/ngAttrSpec.js
Original file line number Diff line number Diff line change
@@ -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('<div existing ng-attr="dynAttr"></div>')($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('<div existing ng-attr="[\'A\', \'B\']"></div>')($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(
'<div existing ' +
'ng-attr="{A: conditionA, B: conditionB(), AnotB: conditionA&&!conditionB()}">' +
'</div>')($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(
'<div existing ' +
'ng-attr="{\'A={{a()}}\': conditionA, \'B={{b}}\': conditionB()}">' +
'</div>')($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('<div ng-attr="attributes"></div>')($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('<div ng-attr="\'value=seventy\'"></div>')($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('<div existing ng-attr="\'A B\'"></div>')($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('<div ng-attr="[\'value=seventy\', \'value2=spork\']"></div>')($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('<div existing ng-attr="dynAttr"></div>')($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('<div ng-attr="dynAttr"></div>')($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('<div ui-panel ui-selected ng-attr="dynAttr"></div>')($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('<div panel bar ng-attr="dynAttr"></div>')($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('<div panel bar ng-attr="dynAttr"></div>')($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('<div ng-attr="dynAttr"></div>')($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('<div ng-attr="dynAttr"></div>')($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('<div ng-attr="{foo:foo}"></div>')($rootScope);
$rootScope.$digest();
expect(element.attr('foo')).toBeUndefined();
}));
});