dumbledad
dumbledad

Reputation: 17527

Populate list item attributes from an AngularJS directive with access to parent's controller

Here is an HTML fragment that I would like to replace with an AnguarJS directive:

<li ng-click="ctrl.navigate('home')"
    ng-mouseenter="ctrl.mouseOver = 'home'"
    ng-mouseleave="ctrl.mouseOver = ''"
    ng-class="{'over' : (ctrl.mouseOver === 'home'), 'selected' : (ctrl.selected === 'home')}">Home</li>

Having defined my directive I was hoping that the above code would become

<li navigation-selection selection-name="home" selection-content="Home" />

To achieve this I added a directive template file navigationselection.html containing this HTML fragment

<li ng-click="ctrl.navigate('{{selectionName}}')"
    ng-mouseenter="ctrl.mouseOver = '{{selectionName}}'"
    ng-mouseleave="ctrl.mouseOver = ''"
    ng-class="{'over' : (ctrl.mouseOver === '{{selectionName}}'), 'selected' : (ctrl.selected === '{{selectionName}}')}">{{selectionTitle}}</li>

and this directive:

    .directive('navigationSelection', [function () {
        return {
            templateUrl: '/navigationselection.html',
            restrict: 'A',
            scope: {
                selectionName: '@',
                selectionTitle: '@'
            }
        };
    }]);

But the resulting HTML has one list item nested within another:

<li navigation-selection="navigation-selection" selection-name="home" selection-title="Thing 1" class="ng-isolate-scope">
    <li ng-click="ctrl.navigate('home')" ng-mouseenter="ctrl.mouseOver = 'home'" ng-mouseleave="ctrl.mouseOver = ''" ng-class="{'over' : (ctrl.mouseOver === 'home'), 'selected' : (ctrl.selected === 'home')}" class="ng-binding">Home</li>
</li>

(N.B. I have added the line breaks for readability.)

If I add replace: true to my directive definition object then it works: the inner list item completely replaces its parent. But, as the AngularJS directive documentation points out, replace is depricated and will be removed in next major release.

In his answer to adam0101’s question How to set a native attribute from AngularJS directive? Mark Rajcok shows how to do this using the directive's link function. And so if I replace the directive template with just

{{selectionTitle}}

and extend the directive with this link function

    .directive('navigationSelection', [function () {
        return {
            templateUrl: '/navigationselection.html',
            restrict: 'A',
            scope: {
                selectionName: '@',
                selectionTitle: '@'
            },
            link: function (scope, element, attrs) {
                attrs.$set('ng-click', "ctrl.navigate('" + attrs.selectionName + "')");
                attrs.$set('ng-mouseenter', "ctrl.mouseOver = '" + attrs.selectionName + "'");
                attrs.$set('ng-mouseleave', "ctrl.mouseOver = ''");
                attrs.$set('ng-class', "{'over' : (ctrl.mouseOver === '" + attrs.selectionName + "'), 'selected' : (ctrl.selected === '" + attrs.selectionName + "')}");
            }
        };
    }]);

It should work. But it doesn't. The resulting HTML looks very similar to my pre-directive version above:

<li navigation-selection="navigation-selection" selection-name="home" selection-title="Home" class="ng-binding ng-isolate-scope" ng-click="ctrl.navigate('home')" ng-mouseenter="ctrl.mouseOver = 'home'" ng-mouseleave="ctrl.mouseOver = ''" ng-class="{'over' : (ctrl.mouseOver === 'home'), 'selected' : (ctrl.selected === 'home')}">Home</li>

but the events, e.g. ng-click, no longer work. I wonder if this is because I've lost access to ctrl when I provided a scope in my directive definition object.

How do I use an AngularJS directive, without replace to add attributes and content to a list item such that the event directives like ng-click can still call methods on the parent's controller?

Upvotes: 1

Views: 823

Answers (1)

dumbledad
dumbledad

Reputation: 17527

There are scope issues; I needed to keep the original scope and not add an isolated scope without passing in the controller.

But I had also misunderstood the life-cycle and so the directives added as attributes by my custom directive are never themselves compiled and I need to do that explicitly. This code fixes things:

    .directive('navigationSelection', ['$compile', function ($compile) {
        return {
            restrict: 'A',
            link: function (scope, element, attrs) {
                element.removeAttr('navigation-selection'); // necessary to avoid infinite compile loop
                element[0].innerText = attrs.selectionTitle;
                attrs.$set('ng-click', "ctrl.navigate('" + attrs.selectionName + "')");
                attrs.$set('ng-mouseenter', "ctrl.mouseOver = '" + attrs.selectionName + "'");
                attrs.$set('ng-mouseleave', "ctrl.mouseOver = ''");
                attrs.$set('ng-class', "{'over' : (ctrl.mouseOver === '" + attrs.selectionName + "'), 'selected' : (ctrl.selected === '" + attrs.selectionName + "')}");
                $compile(element)(scope);
            }
        };
    }]);

Upvotes: 1

Related Questions