AngularJS ng-include inside of Google Maps InfoWindow?

17,545

Solution 1

After you add the content to the DOM, you'll need to find it (maybe with a jQquery selector?), then $compile() it and apply it to the appropriate scope. This will cause Angular to parse your content and act on any directives it finds (like ng-include).

E.g., $compile(foundElement)(scope)

Without more code, it is difficult to give a more precise answer. However, here is a similar question and answer you can look at.

Update: okay, I finally got this to work, and I learned a few things.

google.maps.event.addListener(
      marker,
      'click',
      (function( marker , scope, localLatLng ){
        return function(){
          var content = '<div id="infowindow_content" ng-include src="\'infowindow.html\'"></div>';
          scope.latLng = localLatLng;
          var compiled = $compile(content)(scope);
          scope.$apply();
          infowindow.setContent( compiled[0].innerHTML );
          infowindow.open( Map , marker );
        };//return fn()
      })( marker , scope, scope.markers[i].locations )

I was under the impression that only DOM elements could be $compiled -- i.e., that I first had to add the content to the DOM, and then compile it. It turns out that is not true. Above, I first compile content against the scope, and then add it to the DOM. (I don't know if this might break databinding -- i.e., the $watch()es that were set up by $compile.) I had to set scope.latLng because the ng-included template needs to interpolate {{latLng[0]}} and {{latLng[1]}}. I used innerHTML instead of outerHTML so that only the contents of infowindow.html are inserted.

Plunker.

Update2: Clicking does not work the first time. It appears that 'infowindow.html' is not loaded until a second click (I tried calling scope.$apply() a second time... didn't help). When I had the plunker working, I had inlined the contents of infowindow.html in index.html:

<script type="text/ng-template" id="/test.html">
  <h4>{{latLng[0]}},{{latLng[1]}}</h4>
</script>

I was using that in addListener():

var content = '<div id="infowindow_content" ng-include src="\'/test.html\'"></div>';

I changed the plunker to use the inlined template.

Solution 2

The above answers are solid, but you lose your binding on:

infowindow.setContent( compiled[0].innerHTML );

Do this instead:

infowindow.setContent( compiled[0] );

Otherwise if you have something like <div>{{myVar}}</div>, it won't update if myVar is updated in your app.

Solution 3

Have you tried the compile function? http://docs.angularjs.org/api/ng.$compile

I did not look into angular a lot yet, but I think this could work.

alternatively you could try bootstrapping the stuff. but I dont believe it is the correct way... http://docs.angularjs.org/guide/bootstrap

Share:
17,545

Related videos on Youtube

Jakob Jingleheimer
Author by

Jakob Jingleheimer

Updated on September 15, 2022

Comments

  • Jakob Jingleheimer
    Jakob Jingleheimer over 1 year

    I'm trying to include a template file views/infowindow.html as the content of my InfoWindow from service I wrote to initiate the google maps api:

    for ( var count = locations.length, i = 0; i < count; i++ ) {
    
      var latLng  = locations[i],
        marker = new google.maps.Marker({
          …
        }),
        infowindow = new google.maps.InfoWindow();
    
      google.maps.event.addListener(
        marker,
        'click',
        (function( marker , latLng ){
          return function(){
            var content = '<div ng-include src="\'infowindow.html\'"></div>';
            infowindow.setContent( content );
            infowindow.open( Map , marker );
          }//return fn()
        })( marker , latLng )
      );//addListener
    
    }//for
    

    However, it seems that Angular is not processing content when it is inserted into the InfoWindow (when inspecting the code via Dev Tools, the code that gets inserted is <div ng-include src="'views/infowindow.html'"></div>).

    I was hoping Angular would pre-process my include before it was inserted into the InfoWindow, but alas no.

    Is what I'm trying to do possible?

    I'm thinking that I'll have to somehow cache the template before passing it to infowindow.setContent(), but I don't know how to do that (or if that's even what I should be doing). I would prefer to load the template on the event instead of caching and injecting it for each marker.

    EDIT Looking at $templateCache and a related SO question.

    EDIT 2 Here's a plunk that tries to use $compile (the content of InfoWindow is still <div id="infowindow_content" ng-include src="'infowindow.html'"></div>)


    SOLUTION

    The basis for this came from Mark's answer below. In his solution, the content for InfoWindow is compiled on first click (of any marker) but the InfoWindow does not actually open until another click on any Marker, probably because GoogleMaps is impatient.

    Moving the $compile outside and then passing the compiled template into .addListener solves this problem:

    for ( … ) {
      …
      infowindow = new google.maps.InfoWindow();
      scope.markers …
      var content = '<div id="infowindow_content" ng-include src="\'infowindow.html\'"></div>';
      var compiled = $compile(content)(scope);
    
      google.maps.event.addListener(
        marker,
        'click',
        (function( marker , scope, compiled , localLatLng ){
          return function(){
            scope.latLng = localLatLng;//to make data available to template
            scope.$apply();//must be inside write new values for each marker
            infowindow.setContent( compiled[0].innerHTML );
            infowindow.open( Map , marker );
          };//return fn()
        })( marker , scope, compiled , scope.markers[i].locations )
      );//addListener
    
    }//for
    

    Updated Plunker.

    • theres
      theres over 10 years
      @jacob You probably shoud use infowindow.setContent( compiled[0]); instead of infowindow.setContent( compiled[0].innerHTML );
  • Jakob Jingleheimer
    Jakob Jingleheimer over 11 years
    Hi Luke, so I would do that and cache the 'compiled' output and inject it into the .addListener() function?
  • Jakob Jingleheimer
    Jakob Jingleheimer over 11 years
    Hi Mark, that looks like it might work, thanks! btw, what additional code would you like to see? (for testing, the contents of infowindow.html is <h3>Foo</h3>). I added a bit more to the code in my answer, but that's really all there is (nothing too special).
  • Mark Rajcok
    Mark Rajcok over 11 years
    @jacob, where is the code you show? Is it in an Angular service, directive, controller? Since you are doing DOM manipulation, it should be in a directive. Also, it is not clear what element we can compile... i.e., what "foundElement" will be in my answer above. Well, I went and took a quick look at the Google APIs and (at least in the one example I saw), setContent() or open() (not sure which) created a div with id="content", so you should be able to find that element and $compile it.
  • Jakob Jingleheimer
    Jakob Jingleheimer over 11 years
    The map is initiated in a service (I added a plunk to the question). As far as I can see via Dev Tools, gMaps api does not by default create a div with id="content".
  • Jakob Jingleheimer
    Jakob Jingleheimer over 11 years
    If Angular isn't noticing the ngInclude as it is, why would it notice one that I create? Or are you saying this whole service should really be a directive?
  • Mark Rajcok
    Mark Rajcok over 11 years
    I've been looking at this, but I'm not making much progress. A few notes: scope.$apply() must be called after $compile; addListener() is being passed scope.markers[i] instead of scope(?); yes, use infowindow_content (not content) to find the element to $compile; it doesn't have to be in a directive to work, it is just that normally DOM manipulations are performed by directives, not services.
  • Jakob Jingleheimer
    Jakob Jingleheimer over 11 years
    thanks for the update! Bad news tho: I the plunker doesn't work for me no matter how many times I click the marker :(
  • Jakob Jingleheimer
    Jakob Jingleheimer over 11 years
    Hazzah! (this was my first time using Plunker—I have no idea how it handles versioning/forking/branching). Thanks a tonne!
  • Jakob Jingleheimer
    Jakob Jingleheimer over 11 years
    Mark, I'm going to try moving the content stuff (var content …) outside of the event listener to see if that fixes the issue with first click (it appears you're right: first click triggers the compile and content isn't ready until the second click).
  • John Doe
    John Doe about 11 years
    @MarkRajcok Hi! I had to use pretty much the same compiled[0].innerHTML, then I tried to find something similar and found only this answer. It looks like some kind of a hack to me. Is it an API problem? Why do we need to take the first element and then its innerHTML? I'm learning Angular and maybe I just don't understand something, but this result of a compilation (an array) is a bit ugly.
  • Mark Rajcok
    Mark Rajcok about 11 years
    @JohnDoe, $compile(content) returns a linking function. $compile(content)(scope) (i.e., executing the linking function) returns a wrapped element -- wrapped in jQuery or jqLite. [0] of that wrapped element returns the DOM element (i.e., unwrapped element). So, we $compile the HTML snippet (because it contains Angular directives), then we extract the innerHTML via compiled[0].innerHTML and set infowindow's content with that.
  • John Doe
    John Doe almost 11 years
    @MarkRajcok Thanks, but that's not what I was talking about. I understood what happened here, but this compiled[0].innerHTML part looks simply unnatural. I'm not sure, maybe it's just a rare use case. Thanks anyway.
  • Nick
    Nick about 10 years
    What if you need more than the 0th element. How do you reduce / flatten the rest of the compiled elements into flat html for setContent? compiled[0-5].innerHTML
  • chrisboustead
    chrisboustead over 8 years
    This is very important, as the above answers are useless for anything but use of ng-include.