1

I have been trying to get a dynamic behavior from a composition of directives. Here is the code I am using for sampler.js and index.html:

"use strict";
var app = angular.module("myApp", []);
var Sampler = (function () {
    function Sampler(sampler) {
        this.sampler = sampler;
        this.name = null;
        this.value = null;
        if (sampler) {
            this.name = sampler.name;
            this.value = sampler.value;
        }
    }
    Sampler.prototype.getTemplateFor = function (file) {
        return 'templates/' + name + '/' + file + '.html';
    };
    Sampler.prototype.addA = function () {
        this.value = 'A';
    };
    Sampler.prototype.addB = function () {
        this.value = 'B';
    };
    Sampler.create = function (name) {
        var samplerClass = name + 'Sampler';
        var items = samplerClass.split('.');
        var creator = (window || this);
        for (var i = 0; i < items.length; i++) {
            creator = creator[items[i]];
        }
        if (typeof creator !== 'function') {
            throw new Error('Class named ' + samplerClass + ' not found.');
        }
        var sampler = new creator({
            name: name
        });
        if (!(sampler instanceof Sampler)) {
            throw new Error(name + ' is not instance of Sampler.');
        }
        return sampler;
    };
    return Sampler;
}());
app.directive("sampler", function ($compile) {
    return {
        restrict: "E",
        scope: { result: '=' },
        link: function (scope, element, attributes) {
            var name = !attributes.name ? '' : attributes.name;
            var sampler = Sampler.create(name);
            scope.sampler = sampler;
            var template = '<div class="sampler form-horizontal">' +
                '    <sampler-item ng-if="!!sampler.value" sampler="sampler" />' +
                '    <sampler-new ng-if="!sampler.value" sampler="sampler" />' +
                '</div>';
            if (name) {
                $.ajax({
                    async: false,
                    url: sampler.getTemplateFor('sampler'),
                    success: function (response) { template = response; },
                });
            }
            var content = $compile(template)(scope);
            element.replaceWith(content);
            scope.$watch('sampler.value', function () {
                scope.result = scope.sampler.value;
            });
        }
    };
});
app.directive("samplerNew", function ($compile) {
    return {
        restrict: "E",
        scope: { sampler: '=' },
        link: function (scope, element) {
            var sampler = scope.sampler;
            var template = '\
<div class="new">\
    <button type="button" class="btn btn-default" ng-click="sampler.addA()">Add A</button>\
    <button type="button" class="btn btn-default" ng-click="sampler.addB()">Add B</button>\
</div>';
            if (sampler.name) {
                $.ajax({
                    async: false,
                    url: sampler.getTemplateFor('new'),
                    success: function (response) { template = response; },
                });
            }
            var content = $compile(template)(scope);
            element.replaceWith(content);
        }
    };
});
app.directive("samplerItem", function ($compile) {
    return {
        restrict: "E",
        scope: { sampler: '=' },
        link: function (scope, element) {
            var sampler = scope.sampler;
            var template = '\
<div class="item">\
    Item: {{sampler.value}}\
</div>';
            if (sampler.name) {
                $.ajax({
                    async: false,
                    url: sampler.getTemplateFor('sampler'),
                    success: function (response) { template = response; },
                });
            }
            var content = $compile(template)(scope);
            element.replaceWith(content);
        }
    };
});
<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
</head>
<body ng-app="myApp">
    <sampler result="result"></sampler>
    Expression: {{result}}

    <script src="lib/jquery/jquery-3.1.1.js"></script>
    <script src="lib/angular/js/angular.js"></script>
    <script src="js/directives/sampler.js"></script>
</body>
</html>

When the page loads the output is:

After page loads

After you press a button the expected result should be:

Expected result

But the result is:

Actual result

Please note that I am using link to load the template because I need to load a dynamic template with fallback to a default one.

Thinks works fine if I use the template property of the directive but that does not suits me because of the dynamic template so please do not send this as an answer.

Can anyone help me on that? Thanks

Andre Vianna
  • 1,713
  • 2
  • 15
  • 29
  • you're loading the template asynchronously, but compiling template before it is loaded, is it deliberate? – Yaser Dec 19 '16 at 03:48
  • No the Ajax method is marked as async: false; the template is loaded synchronously. But that is not the problem here. The problem is the behavior of the ng-if. – Andre Vianna Dec 19 '16 at 04:23
  • use `$http` instead `$.ajax`, Its bad practice to mix jQuery & Angular together.. Because runs outside angular context which doesn't intimate angular world to run digest cycle to update bindings – Pankaj Parkar Dec 19 '16 at 04:42
  • Unfortunately $http.get is hard coded to be async (see here: http://stackoverflow.com/questions/13088153/how-to-http-synchronous-call-with-angularjs). Since I don't want to write my own service and $.ajax works fine in this case that is off the table. And again that is not the source of the problem. – Andre Vianna Dec 19 '16 at 16:02

2 Answers2

1

After you $compile the template for the samplerNew directive, then you are using the compiled content to replace the original element - the one that has the ng-if attribute. Hence, ng-if has no effect on the <sampler-new> element because it gets replaced each time it's rendered.

So, try taking your ng-if attribute off the <sampler-new> element and put it on the <div class="new"> element where you compile the samplerNew directive.

The Fix

  1. Go to your sampler directive
  2. Find the string literal assigned to the template variable inside the link function
  3. Cut ng-if="!sampler.value" from the <sampler-new> element
  4. Scroll down to your samplerNew directive
  5. Find the string literal assigned to the template variable inside the link function
  6. Paste ng-if="!sampler.value" on to the <div class="new"> element

Now, when you click Add A or Add B the buttons will disappear and your Item and Expression fields will display.

  • Thanks Stephen that seem to solve the problem. The only problem is that it defers to them content of the tag to handle the behavior. That means that if someone uses a custom template instead of the default one it will have to remember that behavior and add the tag. That sort of breaks the Single Responsibility Principle but for lack of a better solution I will accept this one. Thanks. – Andre Vianna Dec 19 '16 at 16:06
  • Hi Stephen, I found a different answer and I posted it bellow. But as I said your answer is good also. Maybe with both solutions others can choose which one better applies to their problem. Thanks. – Andre Vianna Dec 21 '16 at 16:09
0

I thank the answer by Stephen Tillman (https://stackoverflow.com/users/7181162/stephen-tillman), and it is a working possible solution. But after tinkering with the problem a little more, I found a solution that behaves exactly how I expected.

The main solution is to take possession of the behavior ngIfDirective directive and replace it with our own implementation. Also we have to remember to replace the original element with the new one after doing a replaceWith (instead of an append).

Here is the the code of the sampler.ts the works completely as expected:

// <reference path="../../lib/typings/jquery/jquery.d.ts" />
// <reference path="../../lib/typings/angular/angular.d.ts" />
"use strict";

interface ISampler {
    name: string;
    value: string;

    getTemplateFor(file: string): string;
    addA(): void;
    addB(): void;
    clear(): void;
}

class Sampler implements ISampler {
    public name: string = null;
    public value: string = null;

    constructor(public sampler?: ISampler) {
        if (sampler) {
            this.name = sampler.name;
            this.value = sampler.value;
        }
    }

    public getTemplateFor(file: string): string {
        return 'templates/' + name + '/' + file + '.html';
    }

    public addA(): void {
        this.value = 'A';
    }

    public addB(): void {
        this.value = 'B';
    }

    public clear(): void {
        this.value = null;
    }

    static create(name: string): ISampler {
        var samplerClass = name + 'Sampler'
        var items = samplerClass.split('.');

        var creator = (window || this);
        for (var i = 0; i < items.length; i++) {
            creator = creator[items[i]];
        }

        if (typeof creator !== 'function') {
            throw new Error('Class named ' + samplerClass + ' not found.');
        }

        var sampler = new creator(<ISampler>{
            name: name
        });
        if (!(sampler instanceof Sampler)) {
            throw new Error(name + ' is not instance of Sampler.');
        }

        return sampler;
    }
}

app.directive("sampler", ($compile) => {
    return {
        restrict: "E",
        scope: { result: '=' },
        link: ($scope: ng.IScope | any, $element, $attrs) => {
            var name = !$attrs.name ? '' : $attrs.name;
            var sampler = Sampler.create(name);
            $scope.sampler = sampler;
            var template =
'<div class="sampler form-horizontal">' +
'    <sampler-item ng-if="!!sampler.value" sampler="sampler"></sampler-item>' +
'    <sampler-new ng-if="!sampler.value" sampler="sampler"></sampler-new>' +
'</div>';
            if (name) {
                $.ajax({
                    async: false,
                    url: sampler.getTemplateFor('sampler'),
                    success: (response) => { template = response; },
                });
            }
            var newElement = $compile(template)($scope);
            $element.replaceWith(newElement);
            $element = newElement;
            $scope.$watch('sampler.value', () => {
                $scope.result = $scope.sampler.value;
            });
        }
    }
});

app.directive("samplerNew", (ngIfDirective, $compile) => {
    var ngIf = ngIfDirective[0];
    return {
        restrict: "E",
        priority: ngIf.priority + 1,
        terminal: true,
        scope: { sampler: '=' },
        link: ($scope: ng.IScope | any, $element, $attrs) => {
            var sampler = $scope.sampler;
            var comment = '<!-- show: ' + $attrs.show + ' -->';
            var template = '\
<div class="new">\
    <button type="button" class="btn btn-default" ng-click="sampler.addA()">Add A</button>\
    <button type="button" class="btn btn-default" ng-click="sampler.addB()">Add B</button>\
</div>';
            if (sampler.name) {
                $.ajax({
                    async: false,
                    url: sampler.getTemplateFor('new'),
                    success: (response) => { template = response; },
                });
            }
            $scope.$watch($attrs.ngIf, (isVisible) => {
                var newElement = $compile(isVisible ? template : comment)($scope);
                $element.replaceWith(newElement);
                $element = newElement;
            });
        }
    };
});

app.directive("samplerItem", (ngIfDirective, $compile) => {
    var ngIf = ngIfDirective[0];
    return {
        restrict: "E",
        priority: ngIf.priority + 1,
        terminal: true,
        scope: { sampler: '=' },
        link: ($scope: ng.IScope | any, $element, $attrs) => {
            var sampler = $scope.sampler;
            var comment = '<!-- show: ' + $attrs.show + ' -->';
            var template = '\
<div class="item">\
    Item: {{sampler.value}}<br />\
    <button type="button" class="btn btn-default" ng-click="sampler.clear()">Clear</button>\
</div>';
            if (sampler.name) {
                $.ajax({
                    async: false,
                    url: sampler.getTemplateFor('new'),
                    success: (response) => { template = response; },
                });
            }
            $scope.$watch($attrs.ngIf, (isVisible) => {
                var newElement = $compile(isVisible ? template : comment)($scope);
                $element.replaceWith(newElement);
                $element = newElement;
            });
        }
    };
});
Community
  • 1
  • 1
Andre Vianna
  • 1,713
  • 2
  • 15
  • 29
  • Awesome, Andre! I was wondering what you were going to come up with. You're right, my solution breaks SRP. Yours looks solid and reusable. Good deal, man. Take care. –  Dec 22 '16 at 11:46