SamHuckaby
SamHuckaby

Reputation: 1162

AngularJS : Compile directives inside HTML returned by an API

So I have access to a REST API that I am hitting, that returns the following pre-generated HTML:

<p class="p">
    <sup id="John.3.16" class="v">16</sup>
    <span class="wj">“For </span>
    <span class="wj">God so loved </span>
    <span class="wj">the world,</span>
    <span class="wj">that he gave his only Son, that whoever believes in him should not </span>
    <span class="wj">perish but have eternal life.</span>
</p>

This has presented an interesting new challenge for me in my learning of AngularJS. I have no control over the HTML that is returned from the API, since it's not an API that I built.

What I'm trying to do (and this could be the completely wrong approach) is to build a class directive on the "v" class, so that I can add an ng-click attribute to the verse number and pass the verse information on to another part of my application.

Below is the code I currently have, which doesn't seem to do anything, though I thought it would.

var app = angular.module('ProjectTimothy');

app.filter("sanitize", ['$sce', function($sce) {
    return function(htmlCode){
        return $sce.trustAsHtml(htmlCode);
    }
}]);

app.controller("timothy.ctrl.search", ['$scope', '$http', function($scope, $http){
    $scope.url = "http://apiendpoint.com/";
    $scope.copyright = "";

    $scope.search = function() {
        // Make the request to the API for the verse that was entered
        // Had to modify some defaults in $http to get post to work with JSON data
        // but this part is working well now
        $http.post($scope.url, { "query" : $scope.searchwords, "version": "eng-ESV"})
        .success(function(data, status) {
            // For now I'm just grabbing parts of the object that I know exists
            $scope.copyright = data.response.search.result.passages[0].copyright;
            $scope.result = data.response.search.result.passages[0].text; 
        })
        .error(function(data, status) {
            $scope.data = data || "Request failed";
            $scope.status = status;         
        });

    };
}]);

app.directive("v", ['$compile', function($compile) {
    return {
        restrict: 'C',
        transclude: true,
        link: function(scope, element, attrs) {
            element.html("<ng-transclude></ng-transclude>").show();
            $compile(element.contents())(scope);
        },
        scope: { id:'@' },
        /*template: "<ng-transclude></ng-transclude>",*/
        replace: false
    };
}]);

HTML Template that is being populated with the HTML returned by API:

<div class="bible_verse_search_container" ng-controller="timothy.ctrl.search">
    <div class="input-group">
        <input type="text" class="form-control" placeholder="Bible Verse To Read (i.e. John 11:35)" ng-model="searchwords">
        <span class="input-group-btn">
            <button class="btn btn-default" type="button" ng-click="search()">Search</button>
        </span>
    </div>
    <div class="well" ng-show="copyright" ng-bind-html="copyright | sanitize"></div>
    <div ng-bind-html="result | sanitize"></div>
</div>

So What I was hoping would happen would be that the HTML is populated into the bottom div that binds the html, and then somehow $compile would be called to convert the "v" class sup's into directives that I can modify. Again, I'm pretty new to Angular, so there may be a super easy way to do this like most other things in Anguler that I just haven't found yet.

Really, the end goal is that each verse number is converted into a directive of its own to be able to make it clickable and access the id attribute that it has so that I can send that information with some user content back to my own API.

This feels like a lot of information, so let me know if anything is unclear. I'll be working on it, so if I figure it out first, I'll be sure to update with an answer.

IN PROGRESS

Checked out this question: https://stackoverflow.com/a/21067137/1507210

Now I'm wondering if it would make more sense to try and convert the section where the verse is displayed into a directive, and then making the search controller populate a scope variable with the HTML from the server, and then use that as the template for the directive... think think think

Upvotes: 0

Views: 942

Answers (2)

doog abides
doog abides

Reputation: 2288

Probably the most unwisest of things I have posted here, but it's pretty cool code. I do not know if I recommend actually running this, but here's a jsfiddle

So one of the reasons I call this unwise is because the injected code will run any directives you have not just the one you wanted. There may be many other security risks beyond that as well. But it works fantastically. If you trust the HTML you are retrieving then go for it.

Check out the fiddle for the rest of the code:

function unwiseCompile($compile) {
    return function (scope, element, attrs) {
        var compileWatch = scope.$watch(
          function (scope) { return scope.$eval(attrs.unwiseCompile); },
          function (unwise) {
            element.html(unwise);
            $compile(element.contents())(scope);
            // for better performance, compile once then un-watch
            if (scope.onlyOnce) {
                // un-watch
                compileWatch();
            }
          });
    };
}

Upvotes: 1

MikeJ
MikeJ

Reputation: 2324

I think your second approach--convert the section where the verse is displayed into a directive--would be a nice way of doing it.

You could replace this:

<div ng-bind-html="result | sanitize"></div>

with a directive like this:

<verse-display verse-html="{{result}}"></verse-display>

The directive definition would look like this:

app.directive('verseDisplay', ['$compile', function($compile) {

    function handleClickOnVerse(e) {
        var verseNumber = e.target.id;

        // do what you want with the verse number here
    }

    return {
        restrict: 'E',
        scope: {
            verseHtml: '@'
        },
        replace: true,
        transclude: false,
        template: '<div ng-bind-html="verseHtml | sanitize"></div>',
        link: function(scope, element, attrs) {
            $compile(element.contents())(scope);
            element.on('click', '.v', handleClickOnVerse);
        }
    };
}]);

So you could apply your own click handler to the element.

Here's a fiddle. (Open the console to see the verse number getting logged out.)

Upvotes: 1

Related Questions