Question

I am making a web interface for a board game online. I want to load a Snap.svg map with Snap.load, ie asynchronously.

After it loads, I want to add a watch to a property of the scope, and add some colour to the map according to the property (which is an object). My code is essentially this:

.controller('GameCtrl', [
  '$scope'
  '$routeParams'
  'GameService'
  (
    $scope
    $routeParams
    GameService
  ) ->
    $scope.game = GameService.get($routeParams.gameId)

    $scope.map = Snap("#map")
    Snap.load("img/classical.svg", (data) ->
      console.log "Loaded map!"
      data.select("#provinces").attr
        style: ""
      provinces = data.selectAll("#provinces path")
      for province in provinces
        province.attr
          style: ""
          fill: "#FFFFFF"
          "fill-opacity": "0"
      $scope.map.append(data)

      deregisterWatch = $scope.$watch('game', ->
        console.debug "Game loaded!", $scope.game.data.Id

        for provinceName,unit of $scope.game.data.Phase.Units
          provinceName = provinceName.replace '/', '-'

          province = $scope.map.select("##{provinceName}")

          province.attr
            style: ""
            fill: powers[unit.Nation].colour
            "fill-opacity": "0.8"

          deregisterWatch()
      )

    )

])

Now, the problem is that I want to move the map to its own "class" or file, but it has to know about the $scope somehow, to be able to set the watch after loading.

My first, naïve approach, is to extract the map and just pass in the scope:

define([
  'snap'
], (
  Snap
) ->
  'use strict'

  Map = ($scope, selector, svgPath) ->

    that = {}
    that.provinces = {}

    ...

    that.snap = Snap(selector)
    Snap.load(svgPath, (data) ->
      console.log "Loaded map in Map!"

      data.select("#provinces").attr
        style: ""
      provinces = data.selectAll("#provinces path")
      for province in provinces

        that.provinces[province.attr("id")] = province

        province.attr
          style: ""
          fill: "#FFFFFF"
          "fill-opacity": "0"
      that.snap.append(data)

      deregisterWatch = $scope.$watch('game', ->
        console.debug "Game loaded!", $scope.game.data.Id

        for provinceName,unit of $scope.game.data.Phase.Units
          provinceName = provinceName.replace '/', '-'

          that.colourProvince(provinceName, that.powerColors[unit.Nation].colour)

          deregisterWatch()
      )
    )

    that.colourProvince = (abbr, colour) ->
      ...

    return that

  return Map
)

But I think there must be a more angular way to do this. Should I perhaps make a directive? Any other suggestion?

Was it helpful?

Solution

You should consider writing a directive. Directive are intended to encapsulate reusable components, especially graphic ones.

define [
  'app'  # or use any other way to get to your Angular module.
  'snap'
], (app, Snap) ->

  app.directive 'awesomeMap', ->
    # directive template: will be injected into controller ($element variable)
    template: "<svg></svg>"
    # will remplace hosting element
    replace: true
    # applicable as element and attribute
    restrict: 'EA'
    # here the "parameters" of your directive: can be parsed from HTML, or bound to parent scope (my example).
    scope:
      svgPath: '=?path'
    # controller
    controller: AwesomeMap

  class AwesomeMap

    # Controller dependencies: scope and HTML element
    @$inject: ['$scope', '$element']

    # Controller constructor: bind methods and attributes to current scope
    #
    # @param scope [Object] directive scope
    # @param element [Object] root element's HTML (angular.element)
    constructor: (@scope, element) -
      @snap = Snap(element[0])
      Snap.load(@scope.svgPath, (data) ->
        console.log "Loaded map in Map!">
        # do your stuff here !

Your directive needs to be loaded after Angular's module initialization (don't forget to require it somewhere), and you just have to use it in your HTML.

<div data-ng-controller="AParentScope">
  My awesome map <awsome-map data-path="scopeAttribute"></awsome-map>

(Using CoffeeScript classes with Angular is a challenge, I'm happy to find someone else trying :))

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top