Craig Morgan
Craig Morgan

Reputation: 962

Angularjs $compile(html)($scope) - Error: Converting circular structure to JSON

I have an app where I need to create html and add it to an object array. This html should then be outputted into page. Simple enough. However, I also need to make the html responsive to user actions (click) in which case angular requires me to use $compile to create an angularized template.

See the Plunkr. In this example, what should happen is that when you click on one of the buttons, a popup is generated with the html code embedded in the object, which you can see in the JSON output.

As soon as I do this, I get the error Converting circular structure to JSON. If I don't, the ng-click="Go()" is not called.

SCRIPT

        var template = "<ul class='unstyled'>" +
                "<li ng-click='go()' style='background-color:lightcyan;'><ul class='inline'><li>1...</li><li>1...</li></ul></li>" +
                "<li ng-click='go()'><ul class='inline'><li>1...</li><li>1...</li></ul></li>" +
                "<li ng-click='go()' style='background-color:lightcyan;'><ul class='inline'><li>1...</li><li>1...</li></ul></li>" +
                "</ul>";

       // template = $compile(template)($scope);

        $scope.data = [
            {"id": 1, "html": template},
            {"id": 2, "html": template}
        ];

        $scope.go = function () {
            alert('It works');
        };

        $scope.openPopin = function (html) {
            var e = window.event;
            var popin = document.getElementById('popin');
            var innerdiv = document.getElementById('innerdiv').innerHTML=html;
            popin.style.top= e.pageY - 20+"px";
            popin.style.left = e.pageX - 20+"px";
            popin.style.marginLeft = -500+"px";
            popin.style.marginTop = -100+"px";
        };

        $scope.closePopin = function () {
            var popin = document.getElementById('popin');
            popin.style.top = -500+"px";
            popin.style.left = -500+"px";
        };

HTML

  <div class="popin grey-border" id="popin">
      <button class="close" ng-click="closePopin()">&times;</button>
      <div id="innerdiv"></div>
  </div>

  <pre>{{ data |json }} </pre>

  <br/>
  <table style="float: right;">
      <tr ng-repeat="d in data" id="{{$index}}">
          <td>{{ d.id }} -
              <button class="btn btn-mini btn-info" ng-click="openPopin(d.html)"><i class="icon-info-sign"></i></button>
          </td>
      </tr>
  </table>

Upvotes: 1

Views: 1826

Answers (2)

Craig Morgan
Craig Morgan

Reputation: 962

Thanks towr for your help - see the last comment above

HTML

<script type="text/ng-template" id="cmpbpopin.html">
    <button class="btn btn-mini btn-info"><i class="icon-info-sign"></i></button>
    <div class="popin grey-border">
        <button class="close-button">&times;</button>
        <div></div>
    </div>
</script>

<table style="float: right;">
  <tr ng-repeat="d in data" id="{{$index}}">
    <td>{{ d.id }}</td>
    <td>
        <div cm-pb-popup="d.html"></div>
    </td>
 </tr>
</table>
</body>

SCRIPT

var app = angular.module('app', []);

    app.controller('Ctrl', function ($scope, $compile, $http) {

        var template = "<table class='pblist table table-condensed table-hover'>" +
                "<tr ng-click='go()'><td>1...</td><td>1...</td></tr>" +
                "<tr ng-click='go()'><td>1...</td><td>1...</td></tr>" +
                "<tr ng-click='go()'><td>1...</td><td>1...</td></tr>" +
                "</table>";

        $scope.data = [
            {"id": 1, "html": template},
            {"id": 2, "html": template}
        ];

    });

    app.directive("cmPbPopup", function ($compile, $timeout) {
        return{
            templateUrl: "cmpbpopin.html",
            scope: {
                cmPbPopup: "="
            },
            link: function (scope, elem, attrs) {
                elem.bind("click", function (e) {
                    var popupDiv = elem.find('div');
                    var innerDiv = popupDiv.find('div');
                    var closeButton = popupDiv.find('.close-button')

                    if (e.srcElement.nodeName != 'DIV') {
                        if (e.srcElement.className == 'close-button') {
                            closePopup();
                        } else if(e.srcElement.nodeName == 'TR' || e.srcElement.nodeName == 'TD'){
                            // set values in scope
                            closePopup();
                        }
                        else {
                            innerDiv.html(scope.cmPbPopup);
                            $compile(innerDiv)(scope);
                            popupDiv.css({
                                        'top': e.pageY - e.offsetY + 20,
                                        'left': e.pageX - e.offsetX -10,
                                        'height': 100,
                                        'width': 500,
                                        'marginLeft': -500});
                            $timeout(function (){
                                closeButton.css('display', 'block');
                            },500);
                        }
                    }

                    function closePopup(){
                        popupDiv.css({
                            'height': 0,
                            'width': 0,
                            'marginLeft': 0});
                        $timeout(function (){
                            popupDiv.css({
                                'top': -500,
                                'left': -500
                            });
                        },500);
                    }
                });
            }
        }
    })

CSS

    div.popin {
        position: absolute;
        width: 0;
        height: 0;
        top: -500px;
        left: -500px;
        background-color: #ffffff;
        transition: width 0.5s, height 0.5s, margin-left 0.5s;
        -webkit-transition: width 0.5s, height 0.5s, margin-left 0.5s; /* Safari */
        overflow: hidden;
    }

    div.popin div {
        position: absolute;
        top: 0 !important;
        left: 500px !important;
        width: 470px !important;
        transition: width 0.2s, height 0.2s, margin-left 0.2s;
        -webkit-transition: width 0.2s, height 0.2s, margin-left 0.2s;   
    }

    .close-button{
        width: 20px;
        height: 20px;
        float: right;
        font-size: 20px;
        font-weight: bold;
        line-height: 20px;
        color: #000000;
        text-shadow: 0 1px 0 #ffffff;
        opacity: 0.2;
        filter: alpha(opacity=20);
    }

    .pblist{
        margin-left: 10px !important;
        margin-top: 10px;
        width: 470px;
        float: left;
    }

    .grey-border {
        border: 1px #d3d3d3 solid;
        -webkit-border-radius: 4px;
        -moz-border-radius: 4px;
        border-radius: 4px;
        padding: 3px;
    }

Upvotes: 0

towr
towr

Reputation: 4167

I got it to work (for me) by moving the compile step to the openPopin function, and replacing the style-property changes with a more angular alternative. And I'm also ignoring the window.event which is not cross-browser compatible (and not part of the issue).

app.controller('MainCtrl', function($scope, $compile) {
    var template = "<ul class='unstyled'>" +
            "<li ng-click='go()' style='background-color:lightcyan;'><ul class='inline'><li>1...</li><li>1...</li></ul></li>" +
            "<li ng-click='go()'><ul class='inline'><li>1...</li><li>1...</li></ul></li>" +
            "<li ng-click='go()' style='background-color:lightcyan;'><ul class='inline'><li>1...</li><li>1...</li></ul></li>" +
            "</ul>";

    $scope.data = [
        {"id": 1, "html": template},
        {"id": 2, "html": template}
    ];

    $scope.go = function () {
        console.log("go");
    };

    $scope.openPopin = function (html) {
        var popin = document.getElementById('popin');
        var innerdiv = document.getElementById('innerdiv');
        innerdiv.innerHTML=html;
        $compile(innerdiv)($scope);
        angular.element(popin).css({top:'20px', left:'20px'});
    };

    $scope.closePopin = function () {
        var popin = document.getElementById('popin');
        angular.element(popin).css({top:'-500px', left:'-500px'})
    };
});

So, that's one way to get it working. But the question is, what are you really trying to do, and can't we do it in a more angular way? (Using directives, templates and other tools angular provides.)

Upvotes: 2

Related Questions