Skip to content

Commit 27aae04

Browse files
jelbourncurrensy
authored andcommitted
fix(compiler): assign bindings to controller instance when using an ES6 (angular#10977)
class and preAssignBindingsEnabled == false feat(navBar): add disabled attribute * Originally developed by: @maxjoehnk . I'm just resubmitting * Add disabled attribute to navitem * Update demo to showcase ng-disabled and disabled usage * Add tests * Ignore navbar specific button styling when disabled fixes angular#9667 feat(navBar): add disabled attribute * Implemented suggested changes to follow the convention and use parseAttributeBoolean() function from $mdUtils feat(navBar): add disabled attribute * Implemented suggested changes added MutationObserver and removed unnecessary style changes
1 parent 3b59b20 commit 27aae04

File tree

8 files changed

+1285
-8353
lines changed

8 files changed

+1285
-8353
lines changed

package-lock.json

Lines changed: 1165 additions & 8347 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,4 @@
8585
"test:fast": "gulp karma-fast",
8686
"test:full": "gulp karma"
8787
}
88-
}
88+
}

src/components/navBar/demoBasicUsage/index.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
<md-nav-item md-nav-click="goto('page1')" name="page1">
88
Page One
99
</md-nav-item>
10-
<md-nav-item md-nav-click="goto('page2')" name="page2">
10+
<md-nav-item md-nav-click="goto('page2')" name="page2" ng-disabled="secondTabDisabled">
1111
Page Two
1212
</md-nav-item>
1313
<md-nav-item md-nav-click="goto('page3')" name="page3">
1414
Page Three
1515
</md-nav-item>
16+
<md-nav-item md-nav-click="goto('page4')" name="page4" disabled>
17+
Page Four
18+
</md-nav-item>
1619
<!-- these require actual routing with ui-router or ng-route, so they
1720
won't work in the demo
1821
<md-nav-item md-nav-href="#page4" name="page5">Page Four</md-nav-item>
@@ -29,5 +32,9 @@
2932

3033
<md-checkbox ng-model="disableInkBar">Disable Ink Bar</md-checkbox>
3134

35+
<md-checkbox ng-model="secondTabDisabled" aria-label="Disable Second Tab" style="margin: 5px;">
36+
Disable Second Tab
37+
</md-checkbox>
38+
3239
</md-content>
33-
</div>
40+
</div>

src/components/navBar/navBar.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ MdNavBarController.prototype.onKeydown = function(e) {
388388
/**
389389
* @ngInject
390390
*/
391-
function MdNavItem($mdAria, $$rAF) {
391+
function MdNavItem($mdAria, $$rAF, $mdUtil, $window) {
392392
return {
393393
restrict: 'E',
394394
require: ['mdNavItem', '^mdNavBar'],
@@ -429,6 +429,7 @@ function MdNavItem($mdAria, $$rAF) {
429429
'<md-button class="_md-nav-button md-accent" ' +
430430
'ng-class="ctrl.getNgClassMap()" ' +
431431
'ng-blur="ctrl.setFocused(false)" ' +
432+
'ng-disabled="ctrl.disabled"' +
432433
'tabindex="-1" ' +
433434
navigationOptions +
434435
navigationAttribute + '>' +
@@ -454,11 +455,11 @@ function MdNavItem($mdAria, $$rAF) {
454455
// When accessing the element's contents synchronously, they
455456
// may not be defined yet because of transclusion. There is a higher
456457
// chance that it will be accessible if we wait one frame.
458+
var disconnect;
457459
$$rAF(function() {
458460
var mdNavItem = controllers[0];
459461
var mdNavBar = controllers[1];
460462
var navButton = angular.element(element[0].querySelector('._md-nav-button'));
461-
462463
if (!mdNavItem.name) {
463464
mdNavItem.name = angular.element(element[0]
464465
.querySelector('._md-nav-button-text')).text().trim();
@@ -469,8 +470,30 @@ function MdNavItem($mdAria, $$rAF) {
469470
scope.$apply();
470471
});
471472

473+
if('MutationObserver' in $window) {
474+
var config = {attributes: true, attributeFilter: ['disabled']};
475+
var targetNode = element[0];
476+
var mutationCallback = function(mutationList) {
477+
$mdUtil.nextTick(function() {
478+
mdNavItem.disabled = $mdUtil.parseAttributeBoolean(attrs[mutationList[0].attributeName], false);
479+
});
480+
};
481+
var observer = new MutationObserver(mutationCallback);
482+
observer.observe(targetNode, config);
483+
disconnect = observer.disconnect.bind(observer);
484+
} else {
485+
attrs.$observe('disabled', function (value) {
486+
mdNavItem.disabled = $mdUtil.parseAttributeBoolean(value, false);
487+
});
488+
}
489+
490+
472491
$mdAria.expectWithText(element, 'aria-label');
473492
});
493+
494+
scope.$on('destroy', function() {
495+
disconnect();
496+
})
474497
}
475498
};
476499
}

src/components/navBar/navBar.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ $md-nav-bar-height: 48px;
3838
&:hover {
3939
background-color: inherit;
4040
}
41+
4142
}
4243

4344
md-nav-ink-bar {

src/components/navBar/navBar.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,37 @@ describe('mdNavBar', function() {
176176
.toBe('{"reload":true,"notify":true}');
177177
});
178178

179+
it('should set the disabled attribute', function () {
180+
create('<md-nav-bar>' +
181+
' <md-nav-item md-nav-href="#1" name="tab1">' +
182+
' tab1' +
183+
' </md-nav-item>' +
184+
' <md-nav-item md-nav-href="#2" name="tab2" disabled>' +
185+
' tab2' +
186+
' </md-nav-item>' +
187+
'</md-nav-bar>');
188+
189+
var tabCtrl = getTabCtrl('tab2');
190+
expect(tabCtrl.disabled).toBe(true);
191+
});
192+
193+
it('should observe the disabled attribute', function () {
194+
$scope.tabDisabled = false;
195+
create('<md-nav-bar>' +
196+
' <md-nav-item md-nav-href="#1" name="tab1">' +
197+
' tab1' +
198+
' </md-nav-item>' +
199+
' <md-nav-item md-nav-href="#2" name="tab2" ng-disabled="tabDisabled">' +
200+
' tab2' +
201+
' </md-nav-item>' +
202+
'</md-nav-bar>');
203+
var tabCtrl = getTabCtrl('tab2');
204+
expect(tabCtrl.disabled).toBe(false);
205+
$scope.tabDisabled = true;
206+
$scope.$apply();
207+
expect(tabCtrl.disabled).toBe(true);
208+
});
209+
179210
it('does not update tabs if tab controller is undefined', function() {
180211
$scope.selectedTabRoute = 'tab1';
181212

src/core/services/compiler/compiler.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,13 @@ function MdCompilerProvider($compileProvider) {
369369
* @returns {!Object} Created controller instance.
370370
*/
371371
MdCompilerService.prototype._createController = function(options, injectLocals, locals) {
372+
// The third and fourth arguments to $controller are considered private and are undocumented:
373+
// https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L86
374+
// Passing `true` as the third argument causes `$controller` to return a function that
375+
// gets the controller instance instead returning of the instance directly. When the
376+
// controller is defined as a function, `invokeCtrl.instance` is the *same instance* as
377+
// `invokeCtrl()`. However, then the controller is an ES6 class, `invokeCtrl.instance` is a
378+
// *different instance* from `invokeCtrl()`.
372379
var invokeCtrl = this.$controller(options.controller, injectLocals, true, options.controllerAs);
373380

374381
if (getPreAssignBindingsEnabled() && options.bindToController) {
@@ -379,7 +386,7 @@ function MdCompilerProvider($compileProvider) {
379386
var ctrl = invokeCtrl();
380387

381388
if (!getPreAssignBindingsEnabled() && options.bindToController) {
382-
angular.extend(invokeCtrl.instance, locals);
389+
angular.extend(ctrl, locals);
383390
}
384391

385392
// Call the $onInit hook if it's present on the controller.

src/core/services/compiler/compiler.spec.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,5 +427,50 @@ describe('$mdCompiler service', function() {
427427

428428
});
429429

430+
describe('with respectPreAssignBindingsEnabled and not preAssignBindingsEnabled', function() {
431+
var $mdCompiler, pageScope, $rootScope;
432+
433+
beforeEach(module('material.core'));
434+
435+
beforeEach(module(function($mdCompilerProvider, $compileProvider) {
436+
$mdCompilerProvider.respectPreAssignBindingsEnabled(true);
437+
438+
// preAssignBindingsEnabled is removed in Angular 1.7, so we only explicitly turn it
439+
// on if the option exists.
440+
if ($compileProvider.hasOwnProperty('preAssignBindingsEnabled')) {
441+
$compileProvider.preAssignBindingsEnabled(false);
442+
}
443+
}));
444+
445+
beforeEach(inject(function($injector) {
446+
$mdCompiler = $injector.get('$mdCompiler');
447+
$rootScope = $injector.get('$rootScope');
448+
pageScope = $rootScope.$new();
449+
}));
450+
451+
it('should assign bindings by $onInit for ES6 classes', function(done) {
452+
// This will not work in IE11, but the AngularJS Material CI is only running Chrome.
453+
class PizzaController {
454+
$onInit() { this.isInitialized = true; }
455+
}
456+
457+
var compileResult = $mdCompiler.compile({
458+
template: '<span>Pizza</span>',
459+
controller: PizzaController,
460+
controllerAs: 'pizzaCtrl',
461+
bindToController: true,
462+
locals: {topping: 'Cheese'},
463+
});
464+
465+
compileResult.then(function(compileOutput) {
466+
var ctrl = compileOutput.link(pageScope).scope().pizzaCtrl;
467+
expect(ctrl.isInitialized).toBe(true);
468+
expect(ctrl.topping).toBe('Cheese');
469+
done();
470+
});
471+
472+
$rootScope.$apply();
473+
});
474+
});
430475

431476
});

0 commit comments

Comments
 (0)