angularjstypescriptangularjs-directiveng-showangularjs-controlleras

Multiple directives in same element operating on visibility


I have been struggling with my approach for the following scenario. I have a custom directive authorize where I pass the name of a group. If the current user has this group in his profile then the element will be visible, if not the element will be hidden. Example:

<button class="btn btn-default" role="button"
        ng-click="myVm.edit()"
        authorize="{{myVm.groupName}}"><!--groupName = "accountants"-->
    <span class="fa fa-edit" aria-hidden="true"></span> Edit
</button>

and my original directive in typescript authorize.ts using the link function (because I operate on the DOM)

namespace app.blocks.directives {
    "use strict";

    class AuthorizeDirective implements ng.IDirective {

        public restrict: string = "A";

        public replace: boolean = true;

        constructor(private $compile: ng.ICompileService, private authService: services.IAuthService) {
        }

        public static factory(): ng.IDirectiveFactory {
            const directive = ($compile: ng.ICompileService, authService: services.IAuthService) =>
                new AuthorizeDirective($compile, authService);
            directive.$inject = [
                "$compile",
                "app.services.AuthService"
            ];
            return directive;
        }

        public link(scope: ng.IScope, instanceElement: ng.IAugmentedJQuery, instanceAttributes: ng.IAttributes): void {
            let groupName: string = (<any>instanceAttributes).authorize;
            let element = angular.element(instanceElement);
            let hasGroup: boolean = this.authService.hasGroup(groupName);
            element.attr("ng-show", String(hasGroup));
            //remove the attribute, otherwise it creates an infinite loop.
            element.removeAttr("authorize");
            this.$compile(element)(scope);
            }
        }
    }

    angular
        .module("app.blocks.directives")
        .directive("authorize", AuthorizeDirective.factory());
}

This is working fine, the button is hidden if the authService returns false because the user does not belong to that group (i.e: "accountants").

The problem appears when my DOM element has ng-show or ng-hide directives also. Example:

<button class="btn btn-default" role="button"
        ng-hide="myVm.isDeleted"
        ng-click="myVm.edit()"
        authorize="{{myVm.groupName}}">
    <!--groupName = "accountants"-->
    <span class="fa fa-edit" aria-hidden="true"></span> Edit
</button>

When myVm.isDeleted = true it seems that overrides the result of my directive and the DOM element is displayed (when it shouldn't because the user does not belong to the specified group as per my authorize directive).

I realize there is some priority (by default 0) in directives, when two directives have the same priority they are executed in alphabetical order according to the documentation. This post was very helpful to understand that.

So I have some options here:

  1. Have my authorize directive evaluate the conditional in ng-hide or ng-show in order to compute (i.e: if the ng-hide says that the element should be shown but the user has not the specific group, then the element should be hidden). I could not find a way to access myVm.isDeleted within my directive link's function. If anyone know how I'd be happy with this approach.

  2. Have my authorize directive executed BEFORE any other directive and rely on angular to later on determine visibility according to ng-show or ng-hide (i.e: if my authorize directive determines that the element should be hidden because the user does not belong to the given group, then it should transform the DOM element and make it ng-show="false" for example, so that angular hides the element later on. This approach does not seem to work, the DOM seems correct, I can see that the button has ng-show="false" but for some reason I still see the button on screen so it's as if Angular didn't know that it has to hide that element. The funny thing is that if I move to another tab, and I go back to the same tab (the view is reloaded and the directive re-executed) then it works fine. What's going on?.

I went with the option 2 and this is the code that seems to work properly manipulating the DOM, but Angular does not apply the ng-show directive afterwards therefor the result is not as expected.

public priority: number = 999; //High priority so it is executed BEFORE ng-show directive

public link(scope: ng.IScope, instanceElement: ng.IAugmentedJQuery, instanceAttributes: ng.IAttributes): void {
    let groupName: string = (<any>instanceAttributes).authorize;
    let element = angular.element(instanceElement);
    let ngShow: string = (<any>instanceAttributes).ngShow;
    let ngHide: string = (<any>instanceAttributes).ngHide;
    let hasGroup: boolean = this.authService.hasGroup(groupName);
    let ngHideValue = ngHide ? "!" + ngHide : "";
    let ngShowValue = ngShow ? ngShow : "";
    //if hasGroup, use whatever ng-show or ng-hide value the element had (ng-show = !ng-hide).
    //if !hasGroup, it does not matter what value the element had, it will be hidden.
    if (hasGroup) {
        element.attr("ng-show", (ngShowValue + ngHideValue) || "true");
    } else {
        element.attr("ng-show", "false");
    }
    element.removeAttr("ng-hide");
    //remove the attribute, otherwise it creates an infinite loop.
    element.removeAttr("authorize");
    this.$compile(element)(scope);
}

Solution

  • I'd argue that seeing as your authorize directive basically just controls whether the element that it's placed displays or not, you should just move its logic out into a service that you inject into your controller, and let ng-hide control whether the element displays like it's designed to.

    This will be easier for developers who come later to understand - no one wants to go drilling down into individual directives to find various scattered bits of code that call the server, and your button then just looks like this:

    <button class="btn btn-default" role="button"
        ng-hide="myVm.isDeleted || !myVm.isAuthorized(myVm.groupName)"
        ng-click="myVm.edit()">
        <span class="fa fa-edit" aria-hidden="true"></span> Edit
    </button>
    

    Nice and simple to read.