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

Commit e8066c4

Browse files
committed
feat($compile): explicitly request multi-element directive behaviour
Directives which expect to make use of the multi-element grouping feature introduced in 1.1.6 (e46100f7) must now add the property multiElement to their definition object, with a truthy value. This enables the use of directive attributes ending with the words '-start' and '-end' for single-element directives. BREAKING CHANGE: Directives which previously depended on the implicit grouping between directive-start and directive-end attributes must be refactored in order to see this same behaviour. Before: ``` <div data-fancy-directive-start>{{start}}</div> <p>Grouped content</p> <div data-fancy-directive-end>{{end}}</div> .directive('fancyDirective', function() { return { link: angular.noop }; }) ``` After: ``` <div data-fancy-directive-start>{{start}}</div> <p>Grouped content</p> <div data-fancy-directive-end>{{end}}</div> .directive('fancyDirective', function() { return { multiElement: true, // Explicitly mark as a multi-element directive. link: angular.noop }; }) ``` Closes #5372 Closes #6574 Closes #5370 Closes #8044 Closes #7336
1 parent 048a5f1 commit e8066c4

File tree

7 files changed

+169
-27
lines changed

7 files changed

+169
-27
lines changed

src/ng/compile.js

+34-4
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@
111111
* The directive definition object provides instructions to the {@link ng.$compile
112112
* compiler}. The attributes are:
113113
*
114+
* ### `multiElement`
115+
* When this property is set to true, the HTML compiler will collect DOM nodes between
116+
* nodes with the attributes `directive-name-start` and `directive-name-end`, and group them
117+
* together as the directive elements. It is recomended that this feature be used on directives
118+
* which are not strictly behavioural (such as {@link api/ng.directive:ngClick ngClick}), and which
119+
* do not manipulate or replace child nodes (such as {@link api/ng.directive:ngInclude ngInclude}).
120+
*
114121
* #### `priority`
115122
* When there are multiple directives defined on a single DOM element, sometimes it
116123
* is necessary to specify the order in which the directives are applied. The `priority` is used
@@ -1047,10 +1054,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
10471054
}
10481055

10491056
var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
1050-
if (ngAttrName === directiveNName + 'Start') {
1051-
attrStartName = name;
1052-
attrEndName = name.substr(0, name.length - 5) + 'end';
1053-
name = name.substr(0, name.length - 6);
1057+
if (directiveIsMultiElement(directiveNName)) {
1058+
if (ngAttrName === directiveNName + 'Start') {
1059+
attrStartName = name;
1060+
attrEndName = name.substr(0, name.length - 5) + 'end';
1061+
name = name.substr(0, name.length - 6);
1062+
}
10541063
}
10551064

10561065
nName = directiveNormalize(name.toLowerCase());
@@ -1663,6 +1672,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16631672
}
16641673

16651674

1675+
/**
1676+
* looks up the directive and returns true if it is a multi-element directive,
1677+
* and therefore requires DOM nodes between -start and -end markers to be grouped
1678+
* together.
1679+
*
1680+
* @param {string} name name of the directive to look up.
1681+
* @returns true if directive was registered as multi-element.
1682+
*/
1683+
function directiveIsMultiElement(name) {
1684+
if (hasDirectives.hasOwnProperty(name)) {
1685+
for(var directive, directives = $injector.get(name + Suffix),
1686+
i = 0, ii = directives.length; i<ii; i++) {
1687+
directive = directives[i];
1688+
if (directive.multiElement) {
1689+
return true;
1690+
}
1691+
}
1692+
}
1693+
return false;
1694+
}
1695+
16661696
/**
16671697
* When the element is replaced with HTML template then the new attributes
16681698
* on the template need to be merged with the existing attributes in the DOM.

src/ng/directive/ngIf.js

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
*/
7979
var ngIfDirective = ['$animate', function($animate) {
8080
return {
81+
multiElement: true,
8182
transclude: 'element',
8283
priority: 600,
8384
terminal: true,

src/ng/directive/ngRepeat.js

+1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
202202
var NG_REMOVED = '$$NG_REMOVED';
203203
var ngRepeatMinErr = minErr('ngRepeat');
204204
return {
205+
multiElement: true,
205206
transclude: 'element',
206207
priority: 1000,
207208
terminal: true,

src/ng/directive/ngShowHide.js

+14-8
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,13 @@
156156
</example>
157157
*/
158158
var ngShowDirective = ['$animate', function($animate) {
159-
return function(scope, element, attr) {
160-
scope.$watch(attr.ngShow, function ngShowWatchAction(value){
161-
$animate[value ? 'removeClass' : 'addClass'](element, 'ng-hide');
162-
});
159+
return {
160+
multiElement: true,
161+
link: function(scope, element, attr) {
162+
scope.$watch(attr.ngShow, function ngShowWatchAction(value){
163+
$animate[value ? 'removeClass' : 'addClass'](element, 'ng-hide');
164+
});
165+
}
163166
};
164167
}];
165168

@@ -307,9 +310,12 @@ var ngShowDirective = ['$animate', function($animate) {
307310
</example>
308311
*/
309312
var ngHideDirective = ['$animate', function($animate) {
310-
return function(scope, element, attr) {
311-
scope.$watch(attr.ngHide, function ngHideWatchAction(value){
312-
$animate[value ? 'addClass' : 'removeClass'](element, 'ng-hide');
313-
});
313+
return {
314+
multiElement: true,
315+
link: function(scope, element, attr) {
316+
scope.$watch(attr.ngHide, function ngHideWatchAction(value){
317+
$animate[value ? 'addClass' : 'removeClass'](element, 'ng-hide');
318+
});
319+
}
314320
};
315321
}];

src/ng/directive/ngSwitch.js

+2
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ var ngSwitchWhenDirective = ngDirective({
185185
transclude: 'element',
186186
priority: 800,
187187
require: '^ngSwitch',
188+
multiElement: true,
188189
link: function(scope, element, attrs, ctrl, $transclude) {
189190
ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []);
190191
ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element });
@@ -195,6 +196,7 @@ var ngSwitchDefaultDirective = ngDirective({
195196
transclude: 'element',
196197
priority: 800,
197198
require: '^ngSwitch',
199+
multiElement: true,
198200
link: function(scope, element, attr, ctrl, $transclude) {
199201
ctrl.cases['?'] = (ctrl.cases['?'] || []);
200202
ctrl.cases['?'].push({ transclude: $transclude, element: element });

test/ng/compileSpec.js

+65-15
Original file line numberDiff line numberDiff line change
@@ -5487,6 +5487,40 @@ describe('$compile', function() {
54875487
expect(element.attr('dash-test4')).toBe('JamieMason');
54885488
}));
54895489

5490+
it('should keep attributes ending with -start single-element directives', function() {
5491+
module(function($compileProvider) {
5492+
$compileProvider.directive('dashStarter', function(log) {
5493+
return {
5494+
link: function(scope, element, attrs) {
5495+
log(attrs.onDashStart);
5496+
}
5497+
};
5498+
});
5499+
});
5500+
inject(function($compile, $rootScope, log) {
5501+
$compile('<span data-dash-starter data-on-dash-start="starter"></span>')($rootScope);
5502+
$rootScope.$digest();
5503+
expect(log).toEqual('starter');
5504+
});
5505+
});
5506+
5507+
5508+
it('should keep attributes ending with -end single-element directives', function() {
5509+
module(function($compileProvider) {
5510+
$compileProvider.directive('dashEnder', function(log) {
5511+
return {
5512+
link: function(scope, element, attrs) {
5513+
log(attrs.onDashEnd);
5514+
}
5515+
};
5516+
});
5517+
});
5518+
inject(function($compile, $rootScope, log) {
5519+
$compile('<span data-dash-ender data-on-dash-end="ender"></span>')($rootScope);
5520+
$rootScope.$digest();
5521+
expect(log).toEqual('ender');
5522+
});
5523+
});
54905524
});
54915525

54925526
});
@@ -5545,19 +5579,29 @@ describe('$compile', function() {
55455579
}));
55465580

55475581

5548-
it('should group on nested groups', inject(function($compile, $rootScope) {
5549-
$rootScope.show = false;
5550-
element = $compile(
5551-
'<div></div>' +
5552-
'<div ng-repeat-start="i in [1,2]">{{i}}A</div>' +
5553-
'<span ng-bind-start="\'.\'"></span>' +
5554-
'<span ng-bind-end></span>' +
5555-
'<div ng-repeat-end>{{i}}B;</div>' +
5556-
'<div></div>')($rootScope);
5557-
$rootScope.$digest();
5558-
element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level.
5559-
expect(element.text()).toEqual('1A..1B;2A..2B;');
5560-
}));
5582+
it('should group on nested groups', function() {
5583+
module(function($compileProvider) {
5584+
$compileProvider.directive("ngMultiBind", valueFn({
5585+
multiElement: true,
5586+
link: function(scope, element, attr) {
5587+
element.text(scope.$eval(attr.ngMultiBind));
5588+
}
5589+
}));
5590+
});
5591+
inject(function($compile, $rootScope) {
5592+
$rootScope.show = false;
5593+
element = $compile(
5594+
'<div></div>' +
5595+
'<div ng-repeat-start="i in [1,2]">{{i}}A</div>' +
5596+
'<span ng-multi-bind-start="\'.\'"></span>' +
5597+
'<span ng-multi-bind-end></span>' +
5598+
'<div ng-repeat-end>{{i}}B;</div>' +
5599+
'<div></div>')($rootScope);
5600+
$rootScope.$digest();
5601+
element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level.
5602+
expect(element.text()).toEqual('1A..1B;2A..2B;');
5603+
});
5604+
});
55615605

55625606

55635607
it('should group on nested groups of same directive', inject(function($compile, $rootScope) {
@@ -5749,6 +5793,7 @@ describe('$compile', function() {
57495793
module(function($compileProvider) {
57505794
$compileProvider.directive('foo', function() {
57515795
return {
5796+
multiElement: true
57525797
};
57535798
});
57545799
});
@@ -5766,12 +5811,16 @@ describe('$compile', function() {
57665811
it('should correctly collect ranges on multiple directives on a single element', function () {
57675812
module(function($compileProvider) {
57685813
$compileProvider.directive('emptyDirective', function() {
5769-
return function (scope, element) {
5770-
element.data('x', 'abc');
5814+
return {
5815+
multiElement: true,
5816+
link: function (scope, element) {
5817+
element.data('x', 'abc');
5818+
}
57715819
};
57725820
});
57735821
$compileProvider.directive('rangeDirective', function() {
57745822
return {
5823+
multiElement: true,
57755824
link: function (scope) {
57765825
scope.x = 'X';
57775826
scope.y = 'Y';
@@ -5799,6 +5848,7 @@ describe('$compile', function() {
57995848
module(function($compileProvider) {
58005849
$compileProvider.directive('foo', function() {
58015850
return {
5851+
multiElement: true
58025852
};
58035853
});
58045854
});

test/ng/directive/ngSwitchSpec.js

+52
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,32 @@ describe('ngSwitch', function() {
6666
}));
6767

6868

69+
it('should show all elements between start and end markers that match the current value',
70+
inject(function($rootScope, $compile) {
71+
element = $compile(
72+
'<ul ng-switch="select">' +
73+
'<li ng-switch-when-start="1">A</li>' +
74+
'<li>B</li>' +
75+
'<li ng-switch-when-end>C</li>' +
76+
'<li ng-switch-when-start="2">D</li>' +
77+
'<li>E</li>' +
78+
'<li ng-switch-when-end>F</li>' +
79+
'</ul>')($rootScope);
80+
81+
$rootScope.$apply('select = "1"');
82+
expect(element.find('li').length).toBe(3);
83+
expect(element.find('li').eq(0).text()).toBe('A');
84+
expect(element.find('li').eq(1).text()).toBe('B');
85+
expect(element.find('li').eq(2).text()).toBe('C');
86+
87+
$rootScope.$apply('select = "2"');
88+
expect(element.find('li').length).toBe(3);
89+
expect(element.find('li').eq(0).text()).toBe('D');
90+
expect(element.find('li').eq(1).text()).toBe('E');
91+
expect(element.find('li').eq(2).text()).toBe('F');
92+
}));
93+
94+
6995
it('should switch on switch-when-default', inject(function($rootScope, $compile) {
7096
element = $compile(
7197
'<ng:switch on="select">' +
@@ -80,6 +106,32 @@ describe('ngSwitch', function() {
80106
}));
81107

82108

109+
it('should show all default elements between start and end markers when no match',
110+
inject(function($rootScope, $compile) {
111+
element = $compile(
112+
'<ul ng-switch="select">' +
113+
'<li ng-switch-when-start="1">A</li>' +
114+
'<li>B</li>' +
115+
'<li ng-switch-when-end>C</li>' +
116+
'<li ng-switch-default-start>D</li>' +
117+
'<li>E</li>' +
118+
'<li ng-switch-default-end>F</li>' +
119+
'</ul>')($rootScope);
120+
121+
$rootScope.$apply('select = "1"');
122+
expect(element.find('li').length).toBe(3);
123+
expect(element.find('li').eq(0).text()).toBe('A');
124+
expect(element.find('li').eq(1).text()).toBe('B');
125+
expect(element.find('li').eq(2).text()).toBe('C');
126+
127+
$rootScope.$apply('select = "2"');
128+
expect(element.find('li').length).toBe(3);
129+
expect(element.find('li').eq(0).text()).toBe('D');
130+
expect(element.find('li').eq(1).text()).toBe('E');
131+
expect(element.find('li').eq(2).text()).toBe('F');
132+
}));
133+
134+
83135
it('should show all switch-when-default', inject(function($rootScope, $compile) {
84136
element = $compile(
85137
'<ul ng-switch="select">' +

0 commit comments

Comments
 (0)