3

There seems to be no way to provide data to an Angular controller other than through attributes in the DOM handled by directives (of which ngInit is a handy example).

I'd like to provide other "constructor" data, e.g. objects with functions to my $scope.

Background: We have an existing dashboard-style single page application, where each widget manages a <div>, and widget-instance-specific data is provided as an object along with support functions, etc.. This object data doesn't fit nicely into DOM attributes or ngInit calls.

I can't really come up with a better way to it than to have a global hash, and use an instance-specific unique key. Before calling angular.bootstrap(domElement, ['myApp']), we set up all "constructor" parameters in this global hash under the key and then use

<div ng-init='readInitialValuesFromHash("uniqueKey")'>...</div>

where readInitialValuesFromHash gets all its data from globalHash["uniqueKey"] and stores what it needs it in $scope (possibly just the "uniqueKey").

(What seems like an alternative is to use a directive and jQuery.data(), but jQuery.data uses a global hash behind the scenes)

Of course I can hide the global data in a function, but fundamentally still use a singleton/global variable. This "global hash and pass key as param to ng init trick" just seems like such a hack...

Am I missing something? Is there a better way, given that the widget-instance-specific data is actually more complicated than suitable for inserting in the DOM via directives/attributes due to the legacy dashboard framework?

Are there dangers when putting complicated objects in the $scope as long as they aren't referenced by directives, {{}} or $scope.$watch() calls?

Angular's front page says:

Add as much or as little of AngularJS to an existing page as you like

So in light of that, how should I proceed?


EDIT: Comments have asked to make my question more clear. As an example of a non-trivial constructor parameter, assume I want to give this myObj to the controller, prototypical inheritance, function and all:

var proto = {
    p1: "p1",
    pf: function() {
        return "proto"
    }
};
function MyObj(ost) {
    this.ost = ost;
}
MyObj.prototype=proto;
var myObj = new MyObj("OST");

So I have myObj, and I have a string:

<div ng-app='myApp' ng-controller="MyCtrl">....</div>

I put the string in the DOM, and call angular.bootstrap().

How to I get the real myObj object into MyCtrl's $scope for this <div>, not a serialized/deserialized version/copy of it?

Community
  • 1
  • 1
Peter V. Mørch
  • 13,830
  • 8
  • 69
  • 103
  • Why don't you just use AngularJs routing for passing ```uniqueKey``` parameter to your application? – Andrew Shustariov Jan 15 '14 at 00:33
  • `There seems to be no way to provide data to an Angular controller other than through attributes in the DOM handled by directives` - can you please make it more clear? – Ilan Frumer Jan 15 '14 at 00:41
  • @AndreyShustariov: There are many ways to pass `uniqueKey`. I'm asking if there is a way to avoid using a `globalHash` and `uniqueKey` combo altogether... – Peter V. Mørch Jan 15 '14 at 01:11

3 Answers3

3

Services is what you are looking for.

You can create your own services and then specify them as dependencies to your components (controllers, directives, filters, services), so Angular's dependency injection will take care of the rest.


Points to keep in mind:

  • Services are application singletons. This means that there is only one instance of a given service per injector. Since Angular is lethally allergic to global state, it is possible to create multiple injectors, each with its own instance of a given service, but that is rarely needed, except in tests where this property is crucially important.

  • Services are instantiated lazily. This means that a service will be created only when it is needed for instantiation of a service or an application component that depends on it. In other words, Angular won't instantiate services unless they are requested directly or indirectly by the application.

  • Services (which are injectable through DI) are strongly preferred to global state (what isn't), because they are much more testable (e.g. easily mocked etc) and "safer" (e.g. against accidental conflicts).


Relevant links:


Example:

Depending on your exact requirements, it might be better to create one service to hold all configuration data or create one service per widget. In the latter case, it would probably be a good idea to include all services in a module of their own and specify it as a dependency of your main module.

var services = angular.module('myApp.services', []);
services.factory('widget01Srv', function () {
    var service = {};
    service.config = {...};

    /* Other widget01-specific data could go here,
     * e.g. functionality (but not presentation-related stuff) */
    service.doSomeSuperCoolStuff = function (someValue) {
        /* Send `someValue` to the server, receive data, process data */
        return somePrettyInterestingStuff;
    }
    ...
    return service;
}
services.factory('widget02Srv', function () {...}
...

var app = angular.module('myApp', ['myApp.services']);
app.directive('widget01', function ('widget01Srv') {
    return function postLink(scope, elem, attrs) {
        attrs.$set(someKey, widget01Srv.config.someKey);
        elem.bind('click', function () {
            widget01Srv.doSomeSuperCoolStuff(elem.val());
        });
        ...
    };
});
gkalpak
  • 47,844
  • 8
  • 105
  • 118
  • I went with a slightly modified version of your answer. But the main idea there, use an specific controller for each widget instance, came from this answer. – Peter V. Mørch Jan 16 '14 at 05:14
0

ExpertSystem's answer gave me the hint that I needed. A separate controller instance for each widget. Note how the constructorParameter (==myObj) gets inserted into the controller.

function creatWidgetInstance(name) {
    ....
    var controllerName = name + 'Ctrl';
    // myObj comes from the original question
    var constructorParameter = myObj;
    widgetApp.controller(controllerName, function($scope) {
      $scope.string = constructorParameter;
    });
    ....
    newWidget = jQuery(...);
    newWidget.attr('ng-controller', controllerName);
    angular.bootstrap(newWidget[0], ['widgetApp']);
    ....
}

See it working in a plunker

Perhaps a more beautiful solution is with a separate service too, as in:

function creatWidgetInstance(name) {
    ....
    var controllerName = name + 'Ctrl';
    var serviceName = name + 'Service';
    // myObj comes from the original question
    var constructorParameter = myObj;
    widgetApp.factory(serviceName, function () {
      return { savedConstructorParameter: constructorParameter };
    });
    widgetApp.controller(controllerName,
      [ '$scope', serviceName, function($scope, service) {
          $scope.string = service.savedConstructorParameter;
        }
      ]
    );
    ....
    newWidget = jQuery(...);
    newWidget.attr('ng-controller', controllerName);
    angular.bootstrap(newWidget[0], ['widgetApp']);
    ....
}

See this in a working Plunker

Peter V. Mørch
  • 13,830
  • 8
  • 69
  • 103
  • Although working, your approach strongly deviates from "the Angular way" (e.g. you are doing DOM manipulation in a controller). Basically, you are putting code belonging to directives and services inside controllers. As a result, your components will be considerably less reusable, testable and efficient. (Just my 2 cents...) – gkalpak Jan 16 '14 at 07:47
  • I want to do things "the Angular way". :-) Where am I doing DOM manipulation in the controller? In my second example, all the service(factory) does is store `constructorParameter`/`myObj`. All the controller does is store serviced data. How is this not the angular way? I don't want to be stubborn, but I honestly don't understand. Sure `createWidgetInstance()` does DOM manipulation, but I don't see how this can be avoided. The number and names of the widgets are determined at run-time, I can't hard-code them like in your answer, so something like `createWidgetInstance()` will be needed, no? – Peter V. Mørch Jan 16 '14 at 14:10
  • Just to point out a couple of "non-Angular-way"ish stuff: Using jQuery to create DOM elements should be replaced with using directives. Your `addWidget` should be a service, not a method inside a controller. Furthermore, so tightly coupling two different Angular apps (`main` and `widget`) defeats the whole modularity/reusability approach and must be a nightmare "testing"-wise. Why do you need a new app per widget by the way ? It seems suspiciously strange. (I don't mean to sound like I know everything Angular. In fact I am just getting started myself, so take my comments with a grain of salt.) – gkalpak Jan 16 '14 at 22:23
0

The answer to the question requires backtracking a few assumptions. I thought that the only way to setup $scopewas to do it on a controller. And so the question revolves around how to "provide data to an Angular controller other than through attributes in the DOM handled by directives".

That was misguided.

Instead, one can do:

var scope = $rootScope.$new();
// Actually setting scope.string=scope makes for a horrible example, 
// but it follows the terminology from the rest of the post.
scope.string = myObj;
var element = $compile(jQuery('#widgetTemplate').html())(scope);
jQuery('#widgets').append(element);

See this Plunker for a working example.

Peter V. Mørch
  • 13,830
  • 8
  • 69
  • 103