0

So, I'm fairly new to AngularJS, and I may be approaching this problem incorrectly, especially in the way I'm using directives.

I have a main app that I would like to place some common code in, like a directive to display some messages on an html page. I have a controller that makes a rest API call to validate credentials, which then assigns some data to $scope. I'd like the directive to use that scope data to display the messages in html and do some formatting. From what I've researched, doing this from the controller is not the best practice. I know I could do this directly in the controller and just put a div in the html like:

<div>{{validation.message}}</div>

The simplified version of the html is:

    <html data-ng-app="myApp" data-ng-strict-di>
        <body>
            <div data-ng-view data-ng-controller="MainController">
                <div data-ng-app="StoredAccountsModule" data-ng-controller="AccountController">
                    <button class="btn btn-default" data-ng-click="modalOptions.verify(newAccount)">Verify Credentials</button>
                    <div data-myappmessage></div>
                </div>
            </div>
        </body>
    </html>

The directive is:

angular.module("myApp").directive("myappmessage", function () {
    //&#9989 : &#10006
    return {
        link: function postLink(scope, element, attrs) {

            element.html('<span>' + scope.validation.message + '</span>');
    }
    };
});

I'm sure I'm missing something on how controllers, modules, and directives all connect together.

UPDATE

Ok, so with everyone's comments, I've worked out a lot of the kinks in what I'm trying to do. Right now I am trying to initiate an account validation from a bootstrap modal using a button click that initiates an API call to return the validation information. The modal is being initiated as a service as seen here: http://weblogs.asp.net/dwahlin/building-an-angularjs-modal-service. The modal window part that matters is:

<div data-ng-controller="MainController">
    <div data-simplevalidation></div>
</div>
<div class="modal-footer">
    <button class="btn btn-primary" data-ng-click="modalOptions.ok(newAccount)">{{modalOptions.actionButtonText}}</button>
    <button class="btn btn-warning" data-ng-click="modalOptions.close('cancel')">{{modalOptions.closeButtonText}}</button>
    <button data-ng-controller="MainController" class="btn btn-default" data-ng-click="verifyAccount(newAccount)">Verify Credentials</button>
</div>

And the modal is being launched from:

<div data-ng-controller="StoredAccountsIndexController" style="width:auto;text-align:center;">
    <table style="border:1px solid black;margin:auto;">
        <tfoot>
            <tr>
                <td colspan="2"><button type="button" class="btn btn-primary" data-ng-click="open()">Add New Stored Account</button></td>
            </tr>
        </tfoot>
    </table>
</div>

The directive is now

angular.module("myApp").directive("simplevalidation", function () {
    return {
        template: '<button type="button" class="btn btn-default" data-ng-click=verifyAccount(newAccount)>Verify</button><span></span>',
        link: function ($scope, element, attrs) {
            $scope.$watchGroup(["validation.message", "validation.success"], function () {

                if ($scope.validation.success != null) {
                    if ($scope.validation.success) {
                        element.children("span").html($scope.validation.message + " " + $scope.validation.symbol);
                        element.children("span").css("color", "green")
                    }
                    else {
                        element.children("span").html($scope.validation.message + " " + $scope.validation.symbol);
                        element.children("span").css("color", "red")
                    }
                }

            }, true);
        }
    };
});

Only one app is being declared on the page, myApp. I believe the issue I'm experiencing is with the scope of the modal. If I use the button placed by the directive template, everything works the way it's supposed to. If I use the button in the modal footer, the API call fires, but I never see an update on my scope. The controller that is handling that call is:

angular.module("myApp").controller("MainController", ['$scope', '$timeout', "StoredAccountsFactory", function ($scope, $timeout, StoredAccountsFactory) {
    $scope.validation = {}

    $scope.verifyAccount = function (account) {
        $scope.validation = {};

        StoredAccountsFactory.validateStoredAccount(account).success(function (data, status, headers, config) {
            $scope.validation.message = "Account credentials verified";
            $scope.validation.success = true;
            $scope.validation.symbol = "&#9989;"
        }).error(function (data, status, headers, config) {
            $scope.validation.message = data;
            $scope.validation.success = false;
            $scope.validation.symbol = "&#10006;"
        });
    }
}]);

Any other insight as to what I'm doing wrong here?

I've also tried a directive in this format as I believe is the preferred way to handle directive scope, without any change in outcome.

angular.module("myApp").directive("simplevalidation", function () {
    //add these attributes to the directive element
    //data-validate="verifyAccount(newAccount)" data-validationmessage="validation.message" data-validationsuccess="validation.success"

    return {
        scope: {
            validate: '&',
            validationmessage: '=',
            validationsuccess: '='
        },
        template: '<button type="button" data-ng-click=validate()>Verify</button><span></span>',
        link: function ($scope, element, attrs) {
            $scope.$watchGroup(["validationmessage", "validationsuccess"], function () {

                if ($scope.validationsuccess != null) {
                    if ($scope.validationsuccess) {
                        element.children("span").html($scope.validationmessage + " " + " &#9989;");
                        element.children("span").css("color", "green");
                    }
                    else {
                        element.children("span").html($scope.validationmessage + " " + " &#10006;");
                        element.children("span").css("color", "red");
                    }
                }

            }, true);
    }

    };
});
isherwood
  • 58,414
  • 16
  • 114
  • 157
mhaken
  • 1,075
  • 4
  • 14
  • 28
  • It has to do with the scope you define on your directive. By default, it;s a new scope with prototypal inheritance if i'm not mistaken, but you need a 2 way binding with your $parent controller. Following link will give you a very good understanding of you it works: http://stackoverflow.com/questions/17900201/how-to-access-parent-scope-from-within-a-custom-directive-with-own-scope-in-an – alou Feb 06 '15 at 15:00

2 Answers2

2

You are misunderstanding some concepts here, and I think it overcomplicates what you are trying to do.

1. Modules: Modules are just a way to organize code using dependencies. Once loaded, it doesn't really matter which module hosted which service/controller/directive.

2. App and ng-app: There really should be a single app per page. You can have multiple, but you'd need to manually angular.bootstrap them, and I'm fairly certain that they cannot be nested. Also, apps don't share the same instances of services or scope, so an app is really an isolated execution unit (from Angular's point of view).

So, this nesting of StoredAccountsModule app withing myApp should not be happening:

<html ng-app="myApp" >
  <body>
     <div data-ng-view data-ng-controller="MainController">
        <div data-ng-app="StoredAccountsModule">
           ...
        </div>
     </div>
  </body>
</html>

3. Controllers:

Controllers define and set the View Model (via $scope or with Controller As approach). That statement you made:

I know I could do this directly in the controller and just put a div in the html like:

<div>{{validation.message}}</div>

implying that it is against best practices (as opposed to creating a directive) is plainly wrong.

What is discouraged (and frown upon) is manipulating, accessing, or otherwise making any assumptions about the View (i.e. DOM) in the controller. That is because controller only deals with translation and marshaling of data between backend Models and ViewModels.

In other words, the controller deals with the architecture and the logic of the app - not the manifestation of it in the View.

That characteristic of the controller and Dependency Injection (DI) is what makes controllers highly testable.

4. Directives: A directive is meant to be (for the most part) a reusable piece of functionality that deals directly with the DOM. Directives should not make any assumptions (ideally) about it surrounding HTML or the controller.

Angular does allow for directives to use the outer scope, but it makes them less re-usable. In that regard, your directive myappmessage doesn't provide much value.

Even when authoring a directive, one should not forget about MVVM principles that Angular supports. And as such, directives also have a scope and a controller, and can reuse other and built-in directives for its functionality.

5. Scope: Scope is independent of Modules or Controllers or Directives. So the title of this question "accessing scope from another module" is meaningless. Angular create root scope for an App.

Controllers and Directives share the scope (unless a directive creates an isolate scope), and so a parent controller publishing a variable onto the scope makes the variable available to its subtree via scope inheritance.

Scope inheritance is just one way to communicate between controllers (and some consider it a bad practice), but it's there and can be used (if used properly considering all the constraints of prototypical inheritance nature of the scope).

app.controller("ParentCtrl", function($scope){
   var VM = $scope.VM = ($scope.VM || {});

   VM.isBusy = false;
})
.controller("ChildCtrl", function($scope, $http){
   var VM = $scope.VM = ($scope.VM || {});

   $scope.loadSomething = function(){
      VM.isBusy = true;
      $http.get("something").then(function(d){
         VM.isBusy = false;
      });
   }
});

And in the View:

<div ng-controller="ParentCtrl">
  <div ng-show="VM.isBusy">loading...</div>
  <div ng-controller="ChildCtrl">
    <button ng-click="loadSomething()">load</button>
  </div>
</div>

(These controllers, btw, could have just as easily come from different modules)

Community
  • 1
  • 1
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • Ok, so tracking on what you're saying. As a follow on then, what's the purpose of creating a "root" module as I've seen recommended many places and adding your sub modules as dependencies to that root app? By what you've said, I should just have one module with many controllers or still multiple modules (like one per what I would have defined as an MVC controller) but only load 1 app per page? If I only load 1 app per page, how would I use common code? What I really want is to manipulate html with the directive, like posting html characters in a div. – mhaken Feb 06 '15 at 19:21
  • An app is the root module (I think that is what you mean), and it makes sense to put app-specific dependencies (like config, constants) there. Sub-modules are all about re-usability across apps, if needed, or across dependencies of the same app. Anyway, there are limited circumstances to have multiple same-page apps. Also, some say that Angular would support lazy loading per module – New Dev Feb 06 '15 at 19:48
  • @hakenmt, did this address your question? – New Dev Feb 11 '15 at 23:22
  • For the original question it definitely did, helped me work out the relationship of scope, directives, and controllers. Thanks. For the update I posted though, I'm still having an issue. – mhaken Feb 13 '15 at 00:12
  • @hakenmt, editing is meant to clarify/improve the original question - not to continue adding more questions and issues. SO works best when each question is well-contained and concise. So, if my answer addressed your *original* question, consider marking as accepted and create a new question for any new issue. I also suggesting removing the "Update" section from the question. – New Dev Feb 13 '15 at 00:56
0

You should try and make your directive self contained rather than access the parent scope directly and then put a watch on the expression that you want to react on changes to.

    angular.module("myApp").directive("myappmessage", function () {
        //&#9989 : &#10006
        return {
            scope: { 
              message: '='
            },
            link: function postLink(scope, element, attrs) {                            
                scope.$watch('message', function(value) {
                    element.html('<span>' + value + '</span>');
                });
            }
        };
   });

and then use it as follows:

<div data-myappmessage message="validation.messages"></div>
thedoctor
  • 1,498
  • 1
  • 12
  • 12
  • why manually watching 'message' when you could simply use a directive template with an expression ? – BiAiB Feb 06 '15 at 15:31
  • yes, you're correct in this case you don't need to and the code can be simplified by not having a link function at all, instead just define a template. More generally you probably will have to watch as you may wish to do some complex dom processing in response. – thedoctor Feb 06 '15 at 15:37