I am in the process of writing some unit tests following the conventions in this blog post but I am having trouble accessing my directive's controller functions. I have a directive written with ES6 classes for the directive and the controller. I am using controllerAs to bind my controller class to my directive class. The directive I am trying to write unit tests for looks like so:
// Why is this file included? | Refer to : http://www.michaelbromley.co.uk/blog/350/exploring-es6-classes-in-angularjs-1-x#_section-directives
import directiveFactory from '../../../directivefactory.js';
// ##Directive Definition
class sideNav {
constructor() {
this.template =
`
<!-- SIDENAV -->
<!-- hamburger menu toggle visible when the sidenav menu is toggled shut -->
<span class="glyphicon glyphicon-menu-hamburger side-nav-hamburger dark-hamburger" set-class-when-at-top="fix-to-top" ng-click='vm.test(); vm.toggle();'></span>
<!-- wraps all sidenav menu content -->
<div ng-class='{ show: vm.open }' class="collapsible">
<!-- hamburger menu toggle visible when the sidenav menu is toggled open -->
<span class="glyphicon glyphicon-menu-hamburger side-nav-hamburger light-hamburger" ng-click='vm.test(); vm.toggle();'></span>
<!-- brand-image -->
<div class="side-nav-head" transclude-id="head"></div> <!-- component user content insertion point 1 -->
<!-- navigation links -->
<div class="side-nav-body" transclude-id="body"></div> <!-- component user content insertion point 2 -->
<!-- footer -->
<footer>
</footer>
</div><!-- end collapsible -->
<!-- END SIDENAV -->
`;
this.restrict = 'E';
this.scope = {};
this.bindToController = {
};
this.transclude = true;
this.controller = SideNavController;
this.controllerAs = 'vm';
}
// ###Optional Link Function
link (scope, elem, attrs, ctrl, transclude) {
transclude ((clone) => {
angular.forEach(clone, (cloneEl, value) => {
// If the cloned element has attributes...
if(cloneEl.attributes) {
// Get desired target ID...
var tId = cloneEl.attributes["transclude-to"].value;
// Then find target element with that ID...
var target = elem.find('[transclude-id="' + tId + '"]');
// Append the element to the target
target.append(cloneEl);
}
});
});
}
}
// ###Directive Controller
class SideNavController {
constructor($rootScope) {
this.$rootScope = $rootScope;
// Initiate the menu as closed
this.open = false;
// Upon instantiation setup necessary $rootScope listeners
this.listen();
}
// ####listen()
// *function*
// Setup directive listeners on the $rootScope
listen () {
// Receives an event from the ng-click within the directive template
// for the side-nav-item component
this.$rootScope.$on('navigation-complete', (event) => {
// Upon receiving event, toggle the menu to closed
this.toggle();
});
}
// ####toggle()
// *function*
// Toggle menu open or shut
toggle() {
this.open = !this.open;
}
// ####test()
// *function*
test() { // DEBUG
console.log('tester'); // DEBUG
console.log(this.visible); // DEBUG
console.log(this.open); // DEBUG
}
}
SideNavController.$inject = ['$rootScope'];
export default ['sideNav', directiveFactory(sideNav)];
I take this file and import it along with one other directive component to create a module like this:
import { default as sideNav } from './side-nav/side-nav.js';
import { default as sideNavItem } from './side-nav-item/side-nav-item.js';
let moduleName = 'sideNav';
let module = angular.module(moduleName, [])
// #### Sidebar Nav Components
.directive(...sideNav)
.directive(...sideNavItem);
export default moduleName;
In my unit test I try to mock the controller in beforeEach but no matter if I use the controller name as vm or SideNavController (the former being the controllerAs name and the latter being the actual class nameānot truly sure which is the one I want) I still receive the error: Error: [ng:areq] Argument 'vm/SideNavController' is not a function, got undefined.
This is my unit test:
describe('Side Nav Directive', () => {
let elem, scope, ctrl;
// Mock our side-nav directive
beforeEach(angular.mock.module('sideNav'));
beforeEach(angular.mock.inject(($rootScope, $compile, $controller) => {
// Define the directive markup to test with
elem = angular.element(
`
<div>
<!-- side-nav directive component -->
<side-nav>
<!-- content insertion point 1 -->
<div transclude-to="head">
<img src alt="test_image">
</div>
<!-- content insertion point 2 -->
<div transclude-to="body">
Test Link
</div>
</side-nav>
</div>
`
);
scope = $rootScope.$new();
$compile(elem)(scope);
scope.$digest();
ctrl = $controller('vm', scope);
}));
it("should toggle shut when angular view navigation completes", () => {
expect(ctrl).toBeDefined(); // <----- this errors
});
});
I am truly stumped after referring to many tutorials and blog posts and could really use some insight!
I would actually suggest a slightly different approach to the testing. If the goal is to test the the logic in SideNavController I would consider moving that class to it's own file. That way you can import it in both the directive and the test. Structuring it that way gives you a lot easier access to it since you can test it in complete isolation from the directive itself.
Testing this by compiling markup and creating the entire directive basically turns this into an integration test and is a bit more complex to manage.
In general I find that this makes for more maintainable and useful tests - especially if the goal is to test the controller.
It would be similar to my example here: http://www.syntaxsuccess.com/viewarticle/writing-jasmine-unit-tests-in-es6
The solution I implemented is based on the response TGH made to my original question. I now have the controller class stored in a separate file, which creates a separation of concerns between the directive transclusion functionality and the directive controller functionality. The directive controller file looks like this:
const ROOTSCOPE = new WeakMap();
// ###Directive Controller
class SideNavController {
constructor($rootScope) {
ROOTSCOPE.set(this, $rootScope);
// Initiate the menu as closed
this.open = false;
// Upon instantiation setup necessary $rootScope listeners
this.listen();
}
// ####listen()
// *function*
// Setup directive listeners on the $rootScope
listen () {
// Receives an event from the ng-click within the directive template
// for the side-nav-item component
ROOTSCOPE.get(this).$on('navigation-complete', (event) => {
// Upon receiving event, toggle the menu to closed
this.toggle();
});
}
// ####toggle()
// *function*
// Toggle menu open or shut
toggle() {
this.open = !this.open;
}
}
SideNavController.$inject = ['$rootScope'];
export default SideNavController;
The test file imports the class from the appropriate file and mocks the controller for the test in a beforeEach block. The controller's variables and functions are all available:
// Import the controller to be tested
import SideNavController from './SideNavController.js';
describe('SideNavController', () => {
let $rootScope, vm;
beforeEach(angular.mock.inject((_$rootScope_, _$controller_) => {
$rootScope = _$rootScope_.$new();
vm = _$controller_(SideNavController, {} );
}));
it('should be initialize the open variable to false', () => {
expect(vm.open).toBeDefined();
expect(vm.open).toBeFalsy();
});
it('should listen for a navigation event and call the toggle function when the event is caught', () => {
// Create Jasmine spy to watch toggle function
spyOn(vm, 'toggle');
// Simulate navigation event propagation
$rootScope.$emit('navigation-complete');
expect(vm.toggle).toHaveBeenCalled();
});
});
Related
I am using angular 1.5's new component feature to compartmentalize various things; in particular I have a sidenav slide-out menu.
The sidenav needs to run its initialization code after other components are finished loading. So far I cannot find anything that helps me break this logic apart. At the moment, I am accomplishing this with a messy hack, like this.
assume html body such as this;
<body>
<container>
<navigation></navigation>
<sidenav></sidenav>
</container>
</body>
navigation needs to finish rendering before the sidenav can execute correctly. So in my component files, I am doing this (pseudo code);
SideNav Component
bindings = {};
require = { Container: '^container' };
SideNav Controller
$postLink = function() {
Container['Ready']();
}
Navigation Component
bindings = {};
require = { Container: '^container' };
Navigation Controller
$postLink = function() {
if(Container['Ready'])
Container['Ready']();
}
Container Component
transclude = true;
Container Controller
pending = 2; // controls that must finish loading
Ready = function() {
pending--;
if( pending > 0 )
return;
// code to initialize sidenav via jQuery
}
so basically, there is a counter on the container, and each component that I need to be loaded calls a function on the parent that decrements the counter. If that causes the counter to be 0, then the sidenav is initialized.
This feels really caddywhompus, though. Is there any other way to get some form of notification or behavior that can allow me to initialize the sidenav when it is truly the right time?
You can probably think of a better place to hang these items, but the idea I had with the watch is to do something like this:
<html ng-app="myApp">
<body ng-controller="appController as AppCtrl">
<div>
<component1/></component1>
<component2></component2>
<component3></component3>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script>
angular.module("myApp",[])
.controller("appController", function($rootScope,$scope) {
var ctrl=this;
ctrl.readyForAction = readyForAction;
ctrl.letsParty = letsParty;
$scope.$watch("$rootScope.gotBeer && $rootScope.gotPizza && $rootScope.gotHockey",ctrl.readyForAction)
function readyForAction() {
if ($rootScope.gotBeer && $rootScope.gotPizza && $rootScope.gotHockey) {
ctrl.letsParty()
}
else
{
console.log("Not yet!")
}
};
function letsParty() {
alert("Let's go Red Wings!")
};
})
.component("component1", {
template:"<h1>Beer</h2>",
controller: function($rootScope) {
$rootScope.gotBeer=true;
}
})
.component("component2", {
template: "<h1>Pizza</h1>",
controller: function($rootScope) {
$rootScope.gotPizza = true;
}
})
.component("component3", {
template: "<h1>Hockey</h1>",
controller: function($rootScope) {
$rootScope.gotHockey = true;
}
})
</script>
</body>
</html>
I'm just setting the flags when the controllers are created, but obviously you could set them anywhere. So then you just watch an expression that consists of all of your flags and then when they all evaluate to true you go about your business.
I'm trying out AngularJS and I'm attempting to use TypeScript classes to get prepared for a large project. The problem I'm having is that the this of the controller class is not being binded to the scope inside the portion of the DOM where the ng-controller directive is.
Starting with the following TypeScript code:
angular.module('testApp')
.controller('TypeScriptController', () => new TypeScriptController())
// Controller implementation via TypeScript
class TypeScriptController {
text = "Please click the button";
constructor() {}
buttonClick () {
this.text = "The button was clicked";
}
}
And binding the controller to the DOM using
<main ng-controller="TypeScriptController as TSCtrl">
If I implement this using the standard ES5 function style it works fine (see first half of code snippet below). However the class version does not. Now I can pass in the $scope variable to the controller and bind a property of $scope to this but then the controllerAs syntax in the HTML is ignored. However, I'd like to avoid passing $scope to every controller.
I know that AngularJS 1.3 introduced a bindToController option for directives but I don't see how that could be applied in this context.
Example:
In this example showing the ES5 and TypeScript implementation of a controller. The controller simply contains a method that the ng-click calls to write text below the button. The ES5 version works. The TypeScript version does not. I've also implemented this in a Plunker
angular.module('testApp', [])
.controller('MainController', MainController);
function MainController() {
this.text = "Please click the button";
this.buttonClick = function() {
this.text = "The button was clicked";
};
};
// Compiled from TypeScript source
angular.module('testApp').controller('TypeScriptController', function() {
return new TypeScriptController();
});
// Controller implementation via TypeScript
var TypeScriptController = (function() {
function TypeScriptController() {
this.text = "Please click the button";
}
TypeScriptController.prototype.buttonClick = function() {
this.text = "The button was clicked";
};
return TypeScriptController;
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<body ng-app="testApp">
<main ng-controller="MainController as mainCtrl">
<h1>Hello World!</h1>
<p>
<input type="button" name="testInput" value="Test button" ng-click="mainCtrl.buttonClick()">
</p>
<p>{{mainCtrl.text}}</p>
</main>
<main ng-controller="TypeScriptController as TSCtrl">
<h1>Hello TypeScript!</h1>
<p>
<input type="button" name="testInput" value="Test button" ng-click="TSCtrl.buttonClick()">
</p>
<p>{{TSCtrl.text}}</p>
</main>
</body>
If you want buttonClick to be an instance member (as you did in the ES5 version) instead of a prototype member, use an arrow function:
// Controller implementation via TypeScript
class TypeScriptController {
text = "Please click the button";
constructor() {}
// Arrow function captures 'this'
buttonClick = () => {
this.text = "The button was clicked";
}
}
angular.module('testApp')
.controller('TypeScriptController', () => new TypeScriptController())
You have this wrong for controllerAs syntax. Instead do:
angular.module('testApp')
.controller('TypeScriptController', TypeScriptController)
And angular will run the new when it sees controller as.
As noted by #basarat, the TypeScriptController should be passed directly to the controller.
angular.module('testApp')
.controller('TypeScriptController', TypeScriptController)
The second issue is that the class definition needs to happen before the TypeScriptController is passed to the angular .controller() function. This is because TypeScript compiles classes to variables that contain a function.
// Compiled from TypeScript source
// Note the variable definition
var TypeScriptController = (function() {
function TypeScriptController() {
this.text = "Please click the button";
}
TypeScriptController.prototype.buttonClick = function() {
this.text = "The button was clicked";
};
return TypeScriptController;
})();
This variable needs to be computed before it can be passed to angular. This differs from a typical JavaScript function definition which is available at the start of the program because of JavaScript hoisting.
// MainController function is already defined
angular.module('testApp', [])
.controller('MainController', MainController);
// Available above because defined as "function FuncName()"
function MainController() {
this.text = "Please click the button";
this.buttonClick = function() {
this.text = "The button was clicked";
};
}
Therefore the fix is to define the TypeScript class before passing the controller function.
// Controller implementation via TypeScript
class TypeScriptController {
text = "Please click the button";
constructor() {}
buttonClick() {
this.text = "The button was clicked";
}
}
// Now that class is defined we can pass the controller directly
angular.module('testApp')
.controller('TypeScriptController', TypeScriptController)
I have been trying to create a modal windows that are re-usable with angularJS.
While I am googling I come to know that re-usable modals are created by using
custom directives.
services.
I'm in a confusion which method to follow.
What is the most customized method for creating modal windows in angularJS? and any resources on how to create re-usable modal windows (design patterns for modal windows) in angularJS
Note: Please suggest solutions without angular-bootstrap-ui or bootstrap.
Update:
I am trying to develop a similar kind of screen.
when the user clicks on "select Bus" link a modal window will be displayed.
Title, Content of the modal window is based on the corresponding hyper link.
I've already done this screen by using custom directives with the help of
How to Create a Simple Modal Dialog Directive in Angular.js
But what I am going to rewrite it as a reusable module or directive.
So suggest some design patterns to create re usable custom modal dialog windows using angularJS
Please check the following link for re-usable windows in angular.
That may resolve your issues.
http://www.dwmkerr.com/the-only-angularjs-modal-service-youll-ever-need/
A fairly simple one is ng-modal (https://github.com/adamalbrecht/ngModal), it is a directive, meaning it is highly re-usable. Services are do-able as well, but they are meant to get/set/process data rather than display HTML; hence directives are the way to go.
I've used ng-modal and adding just a little code to it went a long way to make it super-reusable. You can place it in it's own controller and have it always injected so that you can open the modal and show a message or some html.
HTML:
<div ng-controller="modalCtrl">
<modal-dialog show='dialogShown' dialog-title='titleToUse' height='400px' width='75%'>
<p ng-bind="messageToShow"> </p>
<div ng-bind-html="someHTML"></div>
</modal-dialog>
</div>
JS:
myApp.controller('modalCtrl', function($scope){
$scope.titleToUse = "Modal Title";
$scope.messageToShow = "Testing Message"; // default message
$scope.someHTML = "<div>Whoa! A Div!</div>";
$scope.$on('changeMessageEvent', function($event, message){ // listens for change message event and sets new message
$scope.messageToShow = message;
});
});
myApp.controller('someOtherController', function($scope, $rootScope){ // import rootScope
var messageToSendToModal = "New Message!";
$rootScope.$broadcast('changeMessageEvent', messageToSendToModal );
});
Update:
If you want dynamic template and controllers, that is easily to do with ng-modal, you just have to use ng-include:
<div ng-controller="modalCtrl">
<modal-dialog show='dialogShown' dialog-title='titleToUse' height='400px' width='75%'>
<p ng-bind="messageToShow"> </p>
<div ng-bind-html="someHTML"></div>
<!-- some dynamic template -->
<ng-include src="pathToTemplate"></ng-include>
</modal-dialog>
</div>
where in modalCtrl you have
$scope.pathToTemplate = "/path/to/template.html";
that template can contain a controller and can be switched out dynamically via variables.
If anyone is looking for another example I just had a crack at creating a custom modal service and directive myself, with them you can add modals to views like this:
<button ng-click="vm.openModal('custom-modal-1')">Open Modal 1</button>
<modal id="custom-modal-1">
<div class="modal">
<div class="modal-body">
<h1>A Custom Modal!</h1>
<p>
Home page text: <input type="text" ng-model="vm.bodyText" />
</p>
<button ng-click="vm.closeModal('custom-modal-1');">Close</button>
</div>
</div>
<div class="modal-background"></div>
</modal>
Here's the controller that opens and closes the modal:
(function () {
'use strict';
angular
.module('app')
.controller('Home.IndexController', Controller);
function Controller(ModalService) {
var vm = this;
vm.openModal = openModal;
vm.closeModal = closeModal;
initController();
function initController() {
vm.bodyText = 'This text can be updated in modal 1';
}
function openModal(id){
ModalService.Open(id);
}
function closeModal(id){
ModalService.Close(id);
}
}
})();
This is the custom modal service:
(function () {
'use strict';
angular
.module('app')
.factory('ModalService', Service);
function Service() {
var modals = []; // array of modals on the page
var service = {};
service.Add = Add;
service.Remove = Remove;
service.Open = Open;
service.Close = Close;
return service;
function Add(modal) {
// add modal to array of active modals
modals.push(modal);
}
function Remove(id) {
// remove modal from array of active modals
var modalToRemove = _.findWhere(modals, { id: id });
modals = _.without(modals, modalToRemove);
}
function Open(id) {
// open modal specified by id
var modal = _.findWhere(modals, { id: id });
modal.open();
}
function Close(id) {
// close modal specified by id
var modal = _.findWhere(modals, { id: id });
modal.close();
}
}
})();
And this is the custom modal directive:
(function () {
'use strict';
angular
.module('app')
.directive('modal', Directive);
function Directive(ModalService) {
return {
link: function (scope, element, attrs) {
// ensure id attribute exists
if (!attrs.id) {
console.error('modal must have an id');
return;
}
// move element to bottom of page (just before </body>) so it can be displayed above everything else
element.appendTo('body');
// close modal on background click
element.on('click', function (e) {
var target = $(e.target);
if (!target.closest('.modal-body').length) {
scope.$evalAsync(Close);
}
});
// add self (this modal instance) to the modal service so it's accessible from controllers
var modal = {
id: attrs.id,
open: Open,
close: Close
};
ModalService.Add(modal);
// remove self from modal service when directive is destroyed
scope.$on('$destroy', function() {
ModalService.Remove(attrs.id);
element.remove();
});
// open modal
function Open() {
element.show();
$('body').addClass('modal-open');
}
// close modal
function Close() {
element.hide();
$('body').removeClass('modal-open');
}
}
};
}
})();
For more info you can check out this blog post
Using Angular, I'm trying to create a directive that will be placed on a button that will launch a search dialog. There are multiple instances of the search button, but obviously I only want a single instance of the dialog. The dialog should be built from a template URL and have it's own controller, but when the user selects an item, the directive will be used to set the value.
Any ideas on how to create the dialog with it's own controller from the directive?
Here's what I've go so far (basically just the directive)...
http://plnkr.co/edit/W9CHO7pfIo9d7KDe3wH6?p=preview
Here is the html from the above plkr...
Find
Here is the code from the above plkr...
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
var person = {};
person.name = 'World';
$scope.person = person;
$scope.setPerson = function(newPerson) {
person = newPerson;
$scope.person = person;
}
});
app.directive('myFind', function () {
var $dlg; // holds the reference to the dialog. Only 1 per app.
return {
restrict: 'A',
link: function (scope, el, attrs) {
if (!$dlg) {
//todo: create the dialog from a template.
$dlg = true;
}
el.bind('click', function () {
//todo: use the dialog box to search.
// This is just test data to show what I'm trying to accomplish.
alert('Find Person');
var foundPerson = {};
foundPerson.name = 'Brian';
scope.$apply(function () {
scope[attrs.myFind](foundPerson);
});
});
}
}
})
This is as far as I've gotten. I can't quite figure out how to create the dialog using a template inside the directive so it only occurs once and then assign it a controller. I think I can assign the controller inside the template, but first I need to figure out how to load the template and call our custom jQuery plugin to generate the dialog (we have our own look & feel for dialogs).
So I believe the question is, how do I load a template inside of a directive? However, if there is a different way of thinking about this problem, I would be interested in that as well.
I will show you how to do it using bootstrap-ui. (you can modify it easily, if it does not suit your needs).
Here is a skeleton of the template. You can normally bound to any properties and functions that are on directive's scope:
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
... // e.g. <div class="button" ng-click=cancel()></div>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
...
</div>
</div>
</div>
Here is how to create/declare directive in your module:
.directive("searchDialog", function ($modal) {
return {
controller: SearchDialogCtrl,
scope : {
searchDialog: '=' // here you will set two-way data bind with a property from the parent scope
},
link: function (scope, element, attrs) {
element.on("click", function (event) { // when button is clicked we show the dialog
scope.modalInstance = $modal.open({
templateUrl: 'views/search.dialog.tpl.html',
scope: scope // this will pass the isoleted scope of search-dialog to the angular-ui modal
});
scope.$apply();
});
}
}
});
Then controller may look something like that:
function SearchDialogCtrl(dep1, dep2) {
$scope.cancel = function() {
$scope.modalInstance.close(); // the same instance that was created in element.on('click',...)
}
// you can call it from the template: search.dialog.tpl.html
$scope.someFunction = function () { ... }
// it can bind to it in the search.dialog.tpl.html
$scope.someProperty;
...
// this will be two-way bound with some property from the parent field (look below)
// if you want to perform some action on it just use $scope.$watch
$scope.searchDialog;
}
Then it your mark-up you can just use it like that:
<div class="buttonClass" search-dialog="myFieldFromScope">search</div>
I recommend this plugin:
https://github.com/trees4/ng-modal
Demo here:
https://trees4.github.io/ngModal/demo.html
Create a dialog declaratively; and it works with existing controllers. The content of the dialog can be styled however you like.
To set the stage - this is not happening within a single scope, where I can bind a simple attribute. The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope). Further, the button that's clicked is in a child scope about 3 children deep from root.
Here is how I'm currently solving this:
HTML (sitting in root scope):
<ul class="nav-secondary actions"
darthFader fadeDuration="200"
fadeEvent="darthFader:secondaryNav">
Where darthFader is my directive.
Directive:
directive('darthFader',
function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.$on(attrs.fadeevent, function(event,options) {
$(element)["fade" + options.fade || "In"](attrs.fadeduration || 200);
});
}
}
})
So here I'm creating an event handler, specific to a given element, that is calling fadeIn or fadeOut, depending on an option being passed through the event bus (or defaulting to fadeIn/200ms).
I am then broadcasting an event from $rootScope to trigger this event:
$rootScope.$broadcast('darthFader:secondaryNav', { fade: "Out"});
While this works, I'm not crazy about creating an event listener for every instance of this directive (while I don't anticipate having too many darthFader's on a screen, it's more for the pattern I would establish). I'm also not crazy about coupling my attribute in my view with an event handler in both my controller & directive, but I don't currently have a controller wrapping the secondary-nav, so I'd have to bind the secondaryNav to $rootScope, which I don't love either. So my questions:
Is there a way to do this without creating an event handler every time I instantiate my directive? (maybe a service to store a stateful list of elements?)
How should I decouple my view, controller & directive?
Any other obvious questions I'm missing?
Cheers!
You mention in your question
The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope).
I believe if I were to write this same functionality, I would put the element in its own controller--controllers are responsible for managing the intersection of the view and the model, which is exactly what you're trying to do.
myApp.controller('NavController', function($scope) {
$scope.fadedIn = false;
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="fadedIn">
myApp.directive('darthFader', function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
var duration = attrs.fadeDuration || 200;
$scope.$watch(attrs.fadeShown, function(value) {
if (value)
$(element).fadeIn(duration);
else
$(element).fadeOut(duration);
});
}
};
});
If you're worried about sharing the fade in/out state between multiple controllers, you should create a service to share this state. (You could also use $rootScope and event handlers, but I generally find shared services easier to debug and test.)
myApp.value('NavigationState', {
shown: false
});
myApp.controller('NavController', function($scope, NavigationState) {
$scope.nav = NavigationState;
});
myApp.controller('OtherController', function($scope, NavigationState) {
$scope.showNav = function() {
NavigationState.shown = true;
};
$scope.hideNav = function() {
NavigationState.shown = false;
};
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="nav.shown">
<!-- ..... -->
<div ng-controller="OtherController">
<button ng-click="showNav()">Show Nav</button>
<button ng-click="hideNav()">Hide Nav</button>
</div>
Create a custom service, inject it in the controller. Call a method on that service that will do the fade-in/fade-out etc. Pass a parameter to convey additional information.