1

New to angular here. I'm creating a top-nav directive, like so:

<html>
<body ng-app="myApp">
    <top-nav></top-nav>
</body>
</html>

This works fine. However, let's say I have a button outside of the top-nav that needs to call the showLoginDialog() method within the topNav's controller.

In order for this to work, I will need to isolate the controller from the top-nav like so:

<html>
    <body ng-app="myApp">
        <div ng-controller="TopNavController as topNav">
            <top-nav></top-nav>
        </div>
        <!-- assume more markup here.... -->
        <button ng-click="topNav.showLoginDialog()">
    </body>
</html>

My question is: Is this considered bad practice? That is, removing the controller from the directive so that something outside can access it?

Edit: FYI - my "login popup" appears when you click the "Login" button in my top-nav. However, I also want this "login popup" to be able to popup when one clicks on the giant "Register" button in my home page. This is why I've asked how to call it from outside.

Ricky
  • 2,850
  • 5
  • 28
  • 41
  • AFAIK, that is illegal and won't work. –  Sep 24 '15 at 16:48
  • 1
    Why do you need the `as` method? Just put your controller on the body or a mutual parent element and keep your directive separate. – isherwood Sep 24 '15 at 16:50
  • If you need logic from a component outside of it, then that logic does not belong in there. – Ioan Sep 24 '15 at 16:50
  • @isherwood That is how I learned to use controllers. What method do you recommend? – Ricky Sep 24 '15 at 16:51
  • @loan: I just need the ability to show the login dialog from a button outside of the top nav. Is this doable? – Ricky Sep 24 '15 at 16:51

3 Answers3

3

If the showLoginDialog is a common function across components which don't exist in the same hierarchy, then I find it to be less of a headache if you simply implement a service:

app.factory('login', function() {
  return {
    showLoginDialog: function() {
      // whatever
    }
  };
});

In the example you've provided, there won't be any scope inheritance, so you won't be able to access that method from topNav. However, if you place common methods and properties in services/factories, you've now given yourself a mechanism by which you can share a single source of truth for information across your application. This is the more "Angular-ish" way to do it.

Edit

To use the service in your controller, just inject it:

app.controller('topNav', function(login) {
  $scope.showLogin = login.showLoginDialog;
});

app.controller('registerCtrl', function(login) {
  $scope.showLogin = login.showLoginDialog;
});

Depends on the structure of your app, but while the mechanism may change, the policy will be the same.

Josh Beam
  • 19,292
  • 3
  • 45
  • 68
  • Thanks @JoshBeam. Can you please provide an example how I would use this, then? FYI - my "login popup" appears when you click the "Login" button in my top-nav. However, I also want this "login popup" to be able to popup when one clicks on the giant "Register" button in my home page – Ricky Sep 24 '15 at 16:54
  • 1
    @Ricky, no problem, I've edited my answer to show a brief example. I'd suggest looking at the Angular docs though if you need a primer on services and dependency injection. – Josh Beam Sep 24 '15 at 16:56
  • Thanks @JoshBeam. I'll have a look. One last question on this... I noticed it is not prefixed with a dollar sign like $login. Is this because it's a custom and not a system service? – Ricky Sep 24 '15 at 16:57
  • @Ricky, AngularJS internal services use the `$` prefix to distinguish internal services, methods, etc. You can call your service whatever you want, but Angular's convention is to reserve that prefix for their internal services that they provide. – Josh Beam Sep 24 '15 at 16:58
2

Using ng-controller to extend your scope is bad practice.

http://teropa.info/blog/2014/10/24/how-ive-improved-my-angular-apps-by-banning-ng-controller.html

What if you move your directive? You would need to keep applying ng-controllers in different parts of the view and that would get confusing fast.

There are two ways that directives and controllers can talk to each other. One of them Josh Beam already answered which is to use services. The other is to use $broadcast and $emit/$on.

Angular Custom Events using $broadcast, $emit, $on.

For the example you provided, I would have a function on the parent controllers scope that would trigger the directive's showLoginDialog function.

$rootScope.$broadcast('showLogin', data)

Then in your directive's controller do

$rootScope.$on('showLogin', function(e, args){
   // do stuff
});

You must also unregister $rootScope listeners to avoid memory leaks. The way you do that is by calling the function that the $on returns and apply it during the scope's $destroy event.

var cleanfunction = $rootScope.$on('showLogin', showLoginDialog());

$scope.$on('$destroy', function(){
  cleanfunction();
})
Community
  • 1
  • 1
Yeysides
  • 1,262
  • 1
  • 17
  • 27
  • 1
    Events are nice too. If I can add one small thing, it's worth considering a singular location to store your event names (perhaps in an angular `constant`) to prevent the "magic string" problem, but other than that, I am sometimes a fan of this approach :) – Josh Beam Sep 24 '15 at 17:08
  • 1
    Lol, all I mean is doing this: `app.constant('EVENTS', { SHOW_LOGIN: 'showLogin' })`. And you inject `EVENTS` wherever you need it and simply reference the event key name. If you spell the key name wrong, you'll get a `ReferenceError` in the console (which is a good thing, so you know where you messed up your code), rather than the code just silently failing if you accidentally misspell `'showLogin'` somewhere in your code. – Josh Beam Sep 24 '15 at 17:14
  • ooooooh noice. I didn't know you could do this! shweeet. Thanks! @JoshBeam – Yeysides Sep 24 '15 at 17:15
  • @Yeysides: Thanks for showing me this. It's got a jQuery-like feel to it, which I'm very accustom to. However, what is your take on this versus the factory/services method that Josh Beam had mentioned? In my particular case -- i.e. needing to show this "login popup" from my main page's giant "Register" button as well as the "login" button within the top-nav itself, what would you recommend? – Ricky Sep 24 '15 at 17:26
  • @JoshBeam: Same question for you as well :-) Do you have any thoughts on the more angular way to approach this? That is, emit/broadcast versus creating a service? I come from a strong jQuery background, so emit/broadcast feels natural to me. However, I want to ensure that I am learning good habits – Ricky Sep 24 '15 at 17:27
  • 1
    I would say the more idiomatic way is to rely on services as the main source of your data/methods. That approach is easily consistent no matter where you're at in your application. The event approach works (I'm not sure I would necessarily call that the "jQuery" way, even though jQuery does make use of that paradigm), but I would choose one approach and stick with it. Implementing both the event approach and service approach throughout the app could get confusing, and I think the event approach may be more difficult to maintain across the whole app. That's just my two cents :) – Josh Beam Sep 24 '15 at 17:30
  • 1
    Both are good, but I tend to use services to avoid making multiple http requests. So for example if two components need data from a http call, instead of making that call twice, I just have the inital call shove those values in an array/obj and have a service function that returns the collection without making another expensive round trip to the db. @Ricky – Yeysides Sep 24 '15 at 17:30
  • 1
    Thanks guys! This has been a very insightful thread for me! :-) – Ricky Sep 24 '15 at 17:32
0

I can't say that is a bad practice, but it doesn't make sense. If you need a method from a directive accessible outside of it, then the method does not belong in there.

That method should above, perhaps inside of a parent controller / component.

Ioan
  • 5,152
  • 3
  • 31
  • 50
  • Thanks @loan. I've added an edit to my original question to address this. In light of this, how do you think I should proceed? – Ricky Sep 24 '15 at 16:56
  • 1
    A solution is to use events. Just broadcast an event from anywhere, and listen to it in the nav-bar controller. A good example can be found here: http://stackoverflow.com/questions/14502006/working-with-scope-emit-and-on. – Ioan Sep 24 '15 at 16:58