在 Angularjs 中处理模型事件

Handling Model events in Angularjs

我是 Angularjs 的新手,希望确保我没有实施反模式。

我有一个 Global controller 可以通过 factory 获取模型。该模型向位于全局控制器范围内的大约 dozen directives 提供数据。

位于全局控制器内的指令之一是 google 映射,需要在我从服务器获取 lat/long 数据后进行初始化。我在全局控制器中使用 .success() 函数中的 $rootScope.$broadcast("$site:loaded") 宣布模型到达,然后我在指令中使用 $rootScope.$on("$site:loaded") 监听它。

我是否实施了反模式,或者这是 A.O.K。?起初我认为已经有一种方法可以利用,当模型到达时 "know",类似于 Backbone 的 onModelChanged 事件。

关于这个或您在我的代码中看到的任何其他任何提示? (如果它看起来不错,我会接受这样的答案,如果可以的话,我会解释一下为什么它很好)。

这是代码,从基本模板开始:

<!-- Global controller implemented as global. -->
<!DOCTYPE html>
<!--[if lte IE 8]><html class="ie8"><![endif]-->
<!--[if !IE]><!--><html ng-app="juniper" ng-controller="Global as glb" ng-model="glb.site"><!--<![endif]-->
    <head lang="en-US">
        <base href="/ng-test/">
        <meta charset="utf-8" />
        <title></title>

        {% include "DriverSideSiteBundle:Juniper:stylesheets.html.twig" %}
        {% include "DriverSideSiteBundle:Juniper:javascript.html.twig" %}

        {% block head %}
        {% endblock %}
    </head>
    <body vocab="http://schema.org/" itemscope itemtype="http://schema.org/AutomotiveBusiness">
        {#
            These twig files contain <jn-header>
            The scaffolding HTML changes depending on whether the nav is above, below or not
            part of the header
        #}
        {% include 'DriverSideSiteBundle:Juniper:foundation/header.html.twig' with template %}

        <a href="/ng-test/somplace/">create angular 404 page to handle broken links.</a>

        {#
            These twig files contain <jn-sidebar> and <ng-view>
            The scaffolding HTML changes depending on where the nav is and whether there are sidebars
        #}
        {% if template.content.columns == 2 %}
            {% include 'DriverSideSiteBundle:Juniper:foundation/two-column.content.html.twig' with template %}
        {% endif %}

        {% if template.content == 1 %}
            {% include 'DriverSideSiteBundle:Juniper:foundation/one-column.html.twig' with template %}
        {% endif %}

        {# Todo: minify and concatenate into one request. #}
        {% javascripts
            "@DriverSideSiteBundle/Resources/public/vendor/modernizr/modernizr.js"
            "@DriverSideSiteBundle/Resources/public/vendor/angular/angular.js"
            "@DriverSideSiteBundle/Resources/public/vendor/angular-route/angular-route.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/app.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/site.factory.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/global/global.controller.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/homepage/homepage.controller.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/site-description/site-description.controller.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/site-description/site-description.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/my-garage/my-garage.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/services/services.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/coupons/coupons.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/contact-info/contact-info.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/payments/payments.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/certifications/certifications.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/amenities/amenities.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/jn-map/jn-map.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/jn-nav/jn-nav.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/store-hours/store-hours.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/motorev-admin/motorev-admin.directive.js"
        %}
            {# "@DriverSideSiteBundle/Resources/public/juniper/js/libs/underscore.module.js" #}          
            {# "@DriverSideSiteBundle/Resources/public/juniper/js/header/header.directive.js"
            "@DriverSideSiteBundle/Resources/public/juniper/js/footer/footer.directive.js" #}
            <script src="{{ asset_url }}"></script>
        {% endjavascripts %}

        {# Depending on the layout, include this or that version of the angular templates during initial request #}
        {% javascripts '@DriverSideSiteBundle/Resources/public/juniper/js/homepage/homepage.html' output='bundles/driversidesite/juniper/homepage/homepage.html'%}
      {% endjavascripts %}
        {% block angularTemplates %}
        {% endblock %}

    <script type="text/javascript">
        // Trigger foundation's js
        jQuery(document).ready(function () {
            $(document).foundation();
        });
    </script>

    <!-- Replace key with dynamic key -->
    <script type="text/javascript"
      src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDyEYqzoex1QGMLK1YXDye8vIs0o-lQbLQ">
    </script>
    </body>
    </html>

Site 工厂被注入到调用站点的 getSite(:siteId) 方法的全局控制器中。此方法发出 $http 请求。返回的数据设置为Site工厂;我在某处看到那个练习,觉得它看起来不错。

/**
 *  global.controller.js
 */
(function () {
    'use strict';

    var juniper = angular.module('juniper');

    juniper.controller('Global', [ '$rootScope', 'Site', function($rootScope, Site){
        var self = this;

        // Assign the site model to this controller's `site` attr
        this.site = Site;
        // Use the site model's get method to retrieve the site info
        var promise = this.site.getSite(1409); // Todo: Include the Id dynamically


        promise
            .success( $.proxy(function(data, status, headers, config){
                // Bind to the Site obj
                angular.extend(this, data.data);

                // Broadcast when the data arrives so that other directives and controllers can do something when it arrives 
                $rootScope.$broadcast("$site:loaded");

            }, Site)) // Pass in the Site object for the data to bind to
            .error( function(data, status, headers, config) {
                var error = 'error';
                error += '/n data: ' + data;
                error += '/n status: ' + status;
                error += '/n headers: ' + headers;
                error += '/n config: ' + config;

                console.log(error); // Log error
            });
    }]);

}());

Site factory 有一个站点对象,其中包含 getter 和 setter 函数以及从服务器返回的所有数据:

/**
 *  site.factory.js
 */
(function () {
    'use strict';

    var juniper = angular.module('juniper');

    juniper.factory('Site', function($http){

        /**
         *  Site obj is passed to whichever controller needs the site model.
         *  The site obj contains all the methods for getting/setting data as well
         *  as the data itself.
         *
         *  In this case, the site obj is set to the global controller which passes
         *  a reference thru inheritance to all other controllers.
         */
        var Site = {

            /**
             *  Get the site
             *  siteId (int) is the integer id of the site to be retrieved
             */
            getSite: function(siteId) {

                var url = 'http://localhost:8000/api/site/' + siteId;

                return $http({
                    method: 'GET',
                    url: url
                });
            }
        };

        // Return the site obj.
        // You'll need to call the site obj methods once assigning it to a controller's attr.
        return Site;

    });

}());

这就是包含站点工厂获取的模型的全局控制器。这是地图指令和地图模板:

<!-- I didn't specify an ng-model. I didn't see a reason to insert an ng-model since the map directive uses data from the Global controller and nothing else. Is there a way for me to specify that this directive's model is just a portion of the Global model, like by glb.site.location here or by setting the `scope` in the directive declaration ...? Is there a value to doing either of these? -->
<!-- Also didn't specify an ng-controller because I could stick the functions handling this directive's UI in the directive's link function. Is this smart practice, bad practice, or just "a practice"? -->
<div class="Pod">
  <h3 class="Pod__Head show-for-small-only">Location</h3>
  <div class="Pod__Body">
    <div id="map-canvas"></div>
  </div>
</div>

地图指令。请注意,我监听了来自全局控制器的 $scope.$broadcast。我还设置了 scope:false 这给了它全局控制器的范围:

/**
 *  jn-map.directives.js
 */
(function () {
    'use strict';

    // Create template-path variable for easy maintenance
    var path = '/bundles/driversidesite/juniper/';

    var juniper = angular.module('juniper');

    juniper.directive("jnMap", function($rootScope) {
      return {
        restrict: "E",
        transclude: false, // Tell where to transclude the element using the ng-transclude attr
        templateUrl: path + 'jn-map/jn-map.html',
        scope: false,
        link: function(scope, elements, attrs) {

            $rootScope.$on("$site:loaded", $.proxy(function(){
                var mapOptions = {
                  center: {
                    lat: parseFloat(this.site.locations[0].latitude),
                    lng: parseFloat(this.site.locations[0].longitude)
                  },
                  zoom: 8
                };
                var map = new google.maps.Map(document.getElementById('map-canvas'),
                    mapOptions);
            }, scope.glb)); // Pass in the Global controller's model
        }
      }
    });

}());

在我看来,这确实是一种反模式。一旦站点可用,我只会将 map 指令添加到 DOM。

在控制器中:

promise.success(function(data) {
    $scope.loadedSite = data;
}

在模板中:

<jn-map site="loadedSite" ng-if="loadedSite"></jn-map>

该指令将有一个包含站点的范围,并且由于 ng-if:

而只会在站点加载后被调用
juniper.directive("jnMap", function($rootScope) {
  return {
    restrict: "E",
    templateUrl: path + 'jn-map/jn-map.html',
    scope: {
      site: '='
    },
    link: function(scope, element, attrs) {
            var mapOptions = {
              center: {
                lat: parseFloat(scope.site.locations[0].latitude),
                lng: parseFloat(scope.site.locations[0].longitude)
              },
              zoom: 8
            };
            new google.maps.Map(element[0], mapOptions);
          }
  };
});

请注意,此指令在指令的元素中显示地图,而不是在另一个不相关的元素中。 element 是一个类似于 jQuery 的元素包装器对象,因此 element[0] 是原始的 DOM 元素。