1

I'm using ui-router and trying to instantiate a widget that takes as a parameter a DOM element specified by id. This DOM element is in a <div ng-switch> and I want to call the widget constructor when the element is guaranteed to exist.

<div ng-switch on="state">
  <div ng-switch-when="map">
    <div id="map"></div>
  </div>
</div>

From the ui-router lifecycle, I understand that I should hook into $viewContentLoaded. This, however, doesn't work - the DOM element within the ng-switch isn't created at that point:

app.config(['$stateProvider', function ($stateProvider) {
  $stateProvider
    .state('/', {url: '/', templateUrl: 'index.html'})
    .state('map', {url: 'map', templateUrl: 'map.html', controller: 'MapCtrl'})
}]);

app.controller('MapCtrl', ['$rootScope', '$scope', '$state', function MapCtrl($rootScope, $scope, $state) {
  $scope.state = $state.current.name;  // expose the state to the template so we can ng-switch. Apparently there's no better way: https://github.com/angular-ui/ui-router/issues/1482

  $scope.$on('$viewContentLoaded', function mapContentLoaded(event, viewConfig) {
    var mapElement = document.getElementById('map');
    console.log('Attempting to create map into', mapElement);
    var map = new google.maps.Map(mapElement);  // <-- but mapElement will be null!
  });
}]);

What does work is using a setTimeout() of 50ms in the controller, which is brittle, but by that time the DOM element is created. Alternatively, I can set an interval, check for the presence of the map DOM element, and clear the interval when it's found.

What is the proper way of figuring out when an ng-switch has rendered its DOM? This isn't documented.

Here's the Plunkr.

Community
  • 1
  • 1
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
  • you are mixing metaphors/domains by using dom methods and angular methods together, which doesn't work very well with angular. use directives or templates to manipulate mapElement instead of dom methods. – dandavis Oct 27 '14 at 00:48
  • What are you planning to do with the `mapElement`? Whatever it may be, @dandavis is correct you usually only want to do DOM manipulation in directives. – Esteban Felix Oct 27 '14 at 00:56
  • @EstebanFelix: I want to instantiate a Google Map. Just updated the question. How should I go about it? Use ui-router states instead of ng-switch? – Dan Dascalescu Oct 27 '14 at 01:04
  • luckily, there are lots of google map components ready to go for angular, search around for one – dandavis Oct 27 '14 at 01:10
  • @dandavis: I know. The two most popular ones depend on jQuery for no reason, which isn't acceptable for my client. I've already filed GitHub issues for that, e.g. [here](https://github.com/angular-ui/angular-google-maps/issues/801). – Dan Dascalescu Oct 27 '14 at 01:12
  • @DanDascalescu Check out my answer and you can see how to do it "the Angular way" – Esteban Felix Oct 27 '14 at 01:34

1 Answers1

1

I think you're falling in the trap that many experienced front-end developers fall in to when using Angular. In most of other JS libraries we modify the DOM after it's been created and then add functionality to it. However, in Angular the functionality is defined in the HTML. Functionality and interactivity is created by using directives.

In jQuery something like this is fine:

<div id="foobar">
    Click here to do stuff
</div>

<script type="text/javascript">
    $(function () {
        $('#foobar').on('click', function () {
            someService.doStuff();
        });
    });
</script>

Whereas in Angular something like the below is more idiomatic:

<div id="foobar" ng-controller="Main" ng-click="doStuff()">
    Click here to do stuff
</div>

<script type="text/javascript">
    app.controller('Main', ['$scope', 'somerService', function ($scope, someService) {
        $scope.doStuff = function () {
            someService.doStuff();
        }
    }]);
</script>

As for your GoogleMap directive this is by far the simplest way to accomplish it. Albeit this is incredibly basic and may not do everything you need it to.

app.directive('googleMap', [function() {
    return {
      link: function(element) {
        new google.maps.Map(element);
      }
    }
  }
]);

Your map.html:

<div ng-switch on="state">
  <div ng-switch-when="map">
    <div google-map id="map"></div>
  </div>
</div>

As you mentioned however, this would recreate the Google map every time that controller is hit. One way around that is to save off the element and Map api and replacing it on subsequent calls:

app.directive('googleMap', [function () {
    var googleMapElement,
            googleMapAPI;
    return {
        link: function (element) {
            if (!googleMapElement || !googleMapAPI) {
                googleMapAPI = new google.maps.Map(element);
                googleMapElement = element;
            }
            else {
                element.replaceWith(googleMapElement);
            }

        }
    }
}]);
Esteban Felix
  • 1,561
  • 10
  • 21
  • Thanks for the answer! Gotta read the [jQuery -> Angular](http://stackoverflow.com/questions/14994391/how-do-i-think-in-angularjs-if-i-have-a-jquery-background) SO one more time. Will using a directive cause the map to be re-created every time the router lands on that page? That's something I'm trying to avoid. – Dan Dascalescu Oct 27 '14 at 01:43
  • I would be a liar if I said I hadn't read that before writing this response. It's definitely a muscle you need to relearn if you've done jQuery for a long time. Unfortunately it will recreate it every time the switch is activated. – Esteban Felix Oct 27 '14 at 04:20
  • I managed to avoid the re-creation, it seems, by storing `mapDomElement = document.getElementById('map')` into a `map.domElements` `.value` after running `new google.maps.Map(...)`, then running `mapDomElement.parentNode.replaceChild(map.domElements, mapDomElement);` if that value was set, instead of calling the Google Maps constructor. – Dan Dascalescu Oct 27 '14 at 04:32
  • Alas, it seems that [ng-switch isn't the way to go when using ui-router](http://joelhooks.com/blog/2013/07/22/the-basics-of-using-ui-router-with-angularjs/). Rather, one should use nested states and views. – Dan Dascalescu Oct 27 '14 at 04:33
  • @DanDascalescu Ahh you beat me to the punch =P. Yeah, long-term it definitely seems like that is a better way to go. – Esteban Felix Oct 27 '14 at 04:46
  • Thanks again for helping tackle this! The directive code *should* work, but in practice I get an `Uncaught TypeError: Cannot set property 'position' of undefined` at some VM:xxxx point in the Google Maps minified code. The API and the Element are correct; no idea what ends up happening. – Dan Dascalescu Oct 27 '14 at 08:52