14

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.

Community
  • 1
  • 1
Jakob Jingleheimer
  • 30,952
  • 27
  • 76
  • 126
  • Hi there. I'm wondering why your infowindows close when another is opened? I have to use the same infowindow to accomplish that. It seems like you are having one for each, but still they close. – Hawk Aug 16 '13 at 18:47
  • @Hawk, I don't understand your question. I don't use InfoWindow anymore (I use InfoBox) so I might not be able to help you. – Jakob Jingleheimer Aug 16 '13 at 22:30
  • 1
    @jacob You probably shoud use `infowindow.setContent( compiled[0]);` instead of `infowindow.setContent( compiled[0].innerHTML );` – theres Sep 21 '13 at 12:18
  • @theres, yes that's probably better. – Jakob Jingleheimer Sep 23 '13 at 04:21
  • Is there a way to preserve the two-way binding after infowindow.open()? I want to make the template reactive to changes in the scope. For example, I've added a setInteral call to your example that modifies the scope, but this change is not reflected automatically on an open infoWindow. See: http://plnkr.co/edit/Ix92XXSqxru8eMtmCdgH?p=preview – Maor Jun 22 '14 at 08:44
  • @Maor Plunkr is broken so I can't see your code; but setInterval() is outside of Angular's digest cycle, so you would need to call scope.$apply. This article explains why it's necessary and how it works (better than the docs): http://jimhoskins.com/2012/12/17/angularjs-and-apply.html – Jakob Jingleheimer Jun 23 '14 at 16:42
  • Thanks @jacob. I've updated plunker to update the infoWindow on 'mousemove' instead of setInterval. see [HERE](http://plnkr.co/edit/3dapro?p=preview). See that the infoWindow content is not updated on mousemove even though the scope is updated on mousemove. Only when clicking the marker again you will see the updated scope data. – Maor Jun 24 '14 at 19:49
  • @Maor Problem #1, this should be in a Directive, not a Factory/Service. Problem #2, you're still missing the scope.$apply(). It would probably be much easier to use the UI-Map from the AngularUI components. If you need additional functionality, extend their directive(s). – Jakob Jingleheimer Jun 28 '14 at 22:17
  • Notice that to make the above code work with AngularJS v1.2.x you will need to wrap the ng-include with another
    . This is due to a [known open AngularJS bug](https://github.com/angular/angular.js/issues/4505).
    – Maor Jul 01 '14 at 09:32

3 Answers3

13

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.

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 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 `

    Foo

    `). I added a bit more to the code in my answer, but that's really all there is (nothing too special).
    – Jakob Jingleheimer Jan 09 '13 at 19:38
  • @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. – Mark Rajcok Jan 09 '13 at 20:59
  • 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 Jan 10 '13 at 00:42
  • 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? – Jakob Jingleheimer Jan 10 '13 at 01:28
  • 1
    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. – Mark Rajcok Jan 11 '13 at 05:01
  • 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 Jan 11 '13 at 20:03
  • Hazzah! (this was my first time using Plunker—I have no idea how it handles versioning/forking/branching). Thanks a tonne! – Jakob Jingleheimer Jan 11 '13 at 20:17
  • 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). – Jakob Jingleheimer Jan 13 '13 at 20:15
  • @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. – John Doe Apr 24 '13 at 12:26
  • @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. – Mark Rajcok Apr 24 '13 at 15:15
  • @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. – John Doe Apr 24 '13 at 18:44
  • 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 – Nick Feb 12 '14 at 02:36
8

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.

Ethan C
  • 1,408
  • 1
  • 14
  • 26
1

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

Jakob Jingleheimer
  • 30,952
  • 27
  • 76
  • 126
Luke
  • 8,235
  • 3
  • 22
  • 36