Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: MD Khan
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: MD Khan

Exposing A Service Directly On The $scope In AngularJS

By Ben Nadel on

Typically, when I think about the $scope in AngularJS, I think about data that has been explicitly exposed by the Controller for consumption in the view. But what if I had shared data encapsulated within a Service object? Could I simply expose that service on the $scope and then let the View consume it directly? Or would that be considered an anti-pattern?


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To explore this idea, consider a service that models a hierarchical title for the current window. As nested views are rendered (and destroyed), the title can be altered by each Controller. Furthermore, directives may also alter the title as the user interacts with the page at a macro-level.

Ultimately, no matter who updates the title, the View needs to render it using interpolation:

  • <head>
  • <title>{{ someValue }}</title>
  • </head>

The point of this post is to examine how "someValue" is exposed. If the title of the window is being calculated using a shared Service, exposing the service on the $scope means that we can have the View reference the title directly. In the following code, look at the AppController to see how it builds the view-model using the windowTitle service.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController" bn-window-teaser>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <!--
  • Notice that for the window title, we aren't just exposing a simple scope
  • value; rather, we're exposing a service on the scope and then accessing that
  • service directly from the view. This offloads the complexity of the $watch()
  • configuration to the template interpolation mechanism; but, at what cost?
  • -->
  • <title>
  • {{ windowTitle.title }}
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Exposing A Service On The $scope In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="toggleSubview()">Toggle sub-section</a>
  • </p>
  •  
  • <div
  • ng-if="isShowingSubview"
  • ng-controller="SubviewController"
  • class="subview">
  •  
  • <p>
  • Woot, I'm the subview!
  • </p>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.6.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope, windowTitle ) {
  •  
  • // I determine if the subview is being show.
  • $scope.isShowingSubview = false;
  •  
  • // I expose the windowTitle on the scope so that the View can access the
  • // window title directly. If we didn't do this, then I would have to set
  • // up a $watch() function that would translate changes in the windowTitle
  • // object into changes in the view-model (which is what interpolation is
  • // already doing in our case).
  • $scope.windowTitle = windowTitle;
  •  
  • // Set the root page title.
  • windowTitle.push( "My Demo" );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I show or hide the subview depending on the current state.
  • $scope.toggleSubview = function() {
  •  
  • $scope.isShowingSubview = ! $scope.isShowingSubview;
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // I control the subview of the application.
  • app.controller(
  • "SubviewController",
  • function( $scope, windowTitle ) {
  •  
  • // Update the title of the window to reflect the current state.
  • windowTitle.push( "Subview" );
  •  
  • // Since this view is transient, we'll have to revert the window title
  • // back to its previous state when the view is destroyed.
  • $scope.$on(
  • "$destroy",
  • function handleDestroyEvent() {
  •  
  • windowTitle.pop();
  •  
  • // Clear closed-over variable references.
  • $scope = windowTitle = null;
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I alter the window title to provide a teaser when the user leaves the tab.
  • app.directive(
  • "bnWindowTeaser",
  • function( $window, windowTitle ) {
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • // I bind the JavaScript events to the scope.
  • function link( $scope ) {
  •  
  • var win = angular.element( $window )
  • .on( "blur", handleBlurEvent )
  • ;
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // When the user blurs the tab, I show the teaser.
  • function handleBlurEvent( event ) {
  •  
  • // Since we're changing the model, we have to tell AngularJS
  • // about the change.
  • $scope.$apply(
  • function handleApply() {
  •  
  • windowTitle.push( "Whut?! Don't go!", true );
  •  
  • }
  • );
  •  
  • win.on( "focus", handleFocusEvent );
  •  
  • }
  •  
  •  
  • // When the user focuses the tab, I revert the title back to the
  • // previous, non-teaser state.
  • function handleFocusEvent( event ) {
  •  
  • // Since we're changing the model, we have to tell AngularJS
  • // about the change.
  • $scope.$apply(
  • function handleApply() {
  •  
  • windowTitle.pop();
  •  
  • }
  • );
  •  
  • win.off( "focus", handleFocusEvent );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I model the window title of the current page.
  • app.factory(
  • "windowTitle",
  • function() {
  •  
  • // I maintain the collection of title segments.
  • var segments = [];
  •  
  • // I determine if the last item in the segment collection is the only one
  • // that is needed to represent the current window title.
  • var isTerminal = false;
  •  
  • // Expose public methods and data.
  • var exports = {
  • pop: pop,
  • push: push,
  • title: ""
  • };
  •  
  • return( exports );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I remove a title off of the end of the segments.
  • function pop() {
  •  
  • segments.pop();
  •  
  • isTerminal = false;
  •  
  • buildTitle();
  •  
  • }
  •  
  •  
  • // I add a new title to the end of the segments. If the "terminal"
  • // option is used, the given title will be the only one used to build
  • // the current window title.
  • function push( newTitle, newIsTerminal ) {
  •  
  • segments.push( newTitle );
  •  
  • isTerminal = !! newIsTerminal;
  •  
  • buildTitle();
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I compile the title segments down into a single value that represents
  • // the current title of the window.
  • function buildTitle() {
  •  
  • // For terminal titles, use the last segment only.
  • if ( isTerminal ) {
  •  
  • exports.title = segments.slice( -1 ).pop();
  •  
  • // For non-terminal titles, collapse all titles into a single value.
  • } else {
  •  
  • exports.title = segments.join( " -> " );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

In this code, notice that the AppController isn't exposing a simple value on the $scope; rather, it's exposing the entire "windowTitle" service. Then, our View is directly referencing the "windowTitle.title" value. Now, as the shared service is updated by various components within the application, AngularJS will automatically update the title due to the internal watchers that power view-interpolation.

I have an emotional problem with this approach. For a long time, I've only thought of the view-model as containing "local" values. So, the idea of exposing a shared value feels a bit icky. To fix my emotional problems, I could refactor the above code so that it translates the shared value into a local value.

NOTE: I'm only showing the AppController since nothing else changed [substantially].

  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope, windowTitle ) {
  •  
  • // I determine if the subview is being show.
  • $scope.isShowingSubview = false;
  •  
  • // Instead of exposing the windowTitle service directly, we'll expose a
  • // calculated value which we'll have to explicitly keep in sync with the
  • // underlying service.
  • $scope.title = null;
  •  
  • // Check the windowTitle value in EVERY DIGEST to see if it's changed.
  • // And, if so, update the local view-model.
  • $scope.$watch(
  • function calculateTitleChange() {
  •  
  • return( windowTitle.title );
  •  
  • },
  • function handleTitleChange( newValue, oldValue ) {
  •  
  • $scope.title = newValue;
  •  
  • }
  • );
  •  
  • // Set the root page title.
  • windowTitle.push( "My Demo" );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I show or hide the subview depending on the current state.
  • $scope.toggleSubview = function() {
  •  
  • $scope.isShowingSubview = ! $scope.isShowingSubview;
  •  
  • };
  •  
  • }
  • );

Here, rather than exposing the windowTitle service, I'm exposing a "local" title value. This requires me to explicitly keep the shared service and my local title value in sync. To do this, I have to set up a $watch() handler that checks the shared windowTitle service in every digest and save the value locally when it changes.

This overcomes my emotional protest; but, at what cost? Have we actually gained anything?

In terms of $watch() bindings, we've actually lost. In the first approach we only have one $watch() on the title - the one AngularJS uses in the view interpolation. Now, we actually have two - the view interpolation (which we still have) plus the one in our Controller that transforms the shared value into the local value.

In terms of "contracts" there's no difference. In the first approach, the Controller and the View agree on a contract of intent that "windowTitle.title" will be available on the scope. In the latter approach, the contract is the same, only the "key" is different - title vs. windowTitle.title.

In fact, the contract between the Controller and the Service is also the same. In the first approach, the Controller and Service agree on a contract that windowTitle.title exists (and can be exposed). In the latter approach, the explicit $watch() binding makes the Controller take on the same contract.

Now, when it comes to "intent," things get interesting. In the latter approach, with the $watch() binding, the intent is more explicit; you see that a specific service value is being copied into a specific local value. But, due to the noise of the watch functions, I could argue that the intent in the first approach is actually more obvious.

So, exposing a service directly on the $scope - anti-pattern or clean integration? I have objections; but, I think they are all emotional. It seems that exposing a service on the $scope can reduce noise and total $watch() bindings, which means that it's easier to understand and actually more efficient, performance-wise. I think I just need to break-through my emotional blocks.




Reader Comments

Disclaimer: I'm only generally familiar with AngularJS.

That said, in any MVC context, I'm hesitant to allow the view to directly call a service. It seems to me to break the encapsulation of each layer. I see it as the Controller's job to process/gather data based on the request and just return data. Even if it seems innocuous in your use case, I've been bitten enough by not properly separating concerns that I just play it safe.

Incidentally, I have the same opinion of ORMs. Just because you can pass the ORM directly to other tiers does not make it a good idea. The repository pattern is far safer. At worst, you've written a couple of extra classes. At best, you save yourself major pain and effort when your database model changes.

Reply to this Comment

@Matt,

While I think that makes sense in a Server context, the difference on the client is that the rendering isn't a one-time-thing. When I compile a view on the server, its easy enough to grab data from various serves and then render some HTML and return it. But, in this context, the view has to remain up-to-date over time as other parts of the application alter the shared service. That's the only reason that I think _maybe_ it makes sense.

But, I go back and forth; I've never actually tried this in a production app.

And, to make one more thing clear, I would ONLY do this for *reading* data. I would never let the View *write* data directly to a service; then you're bypassing the Controller in a different kind of way.

Reply to this Comment

@Ben

And here's where I think my inexperience with Angular is showing- although I've started going through Pluralsight vids, since my new job uses Angular pretty heavily.

Specifically, I'm not sure what you mean by "as other parts of the application alter the shared service". In the way I'm used to MVC, which is all server-side, the idea of anything changing a service *probably* means that I've built something else incorrectly.

But then, we could very well be using the term "service" differently. Maybe I'll know more after the first couple of videos.

Reply to this Comment

Honestly.. if I heard "hey, so we're going to put the service on the scope" I'd panic a little and have an instant urge to fight that decision.. But the more I think about it..

The window title is a great example, and there's many more like it. If you're going to have to do the same thing to prepare an object on the scope, you're either doing it wrong, it's in the wrong place, or there's a better way to do it. A service, which can encapsulate away all the logic to prepare something like a window title, I could see using that on the scope in certain scenarios.

One downside I would imagine is that it becomes part of the digest? Or was it already part of the digest? Do items only become part of the digest if they're associated to a scope object? If our service becomes part of the digest, do all it's properties become observed?

Imagine our service is bloated, and we put it on the scope. This likely wouldn't happen, but just play along for a second. Would our bloated service fall victim to the digest cycle, and then expose properties that we don't want observed, to become observed? If our service on scope has a property that holds a collection of 10,000 items, does that get watched due to the association with the scope?

Reply to this Comment

Another question, why doesn't

`$scope.windowTitleValue = WindowTitle.title`

work? I "know" it doesn't work, but I don't "know" why it doesn't work. Is it because a `$scope.property = "something"` is monitored for changes in reference, and $scope.property = somethingElse.property doesn't actually change reference when the value changes? Is there any way to force this assignment to be observed by equality watchers rather than reference watchers?

More on the 3 depths of watchers: http://teropa.info/blog/2014/01/26/the-three-watch-depths-of-angularjs.html

Reply to this Comment

Hi Ben, I know that you wrote about general problem however in that particular case there is nice and elegant solution:

$scope.$on('$stateChangeSuccess', function(event, toState) {
if (toState.data && toState.data.pageTitle) {
$scope.pageTitle = toState.data.pageTitle;
}
});

This UI Router example code that can be placed in one place assign "local" scope value.

Regards!

Reply to this Comment

@Matt,

In this case, I guess I'm generally referring to a Singleton that is neither View nor Controller. Here, the "windowTitle" service is just a simple object that helps maintain a single "title". And, that title can be changed by various controllers and directives in the app.

I hesitate to call it part of the "model" as I always consider the model to be transient objects that get created and destroyed. As this sticks around for the duration of the app, I tend to call it a service... but here's were I'm probably misusing terminology :)

Reply to this Comment

@Atticus,

Excellent question regarding the digest. The purpose of the digest is perform a "dirty check" of the bound data. That is, to look at all the $watch() bindings and check to see if the expression has changed between one action and another.

In AngularJS, these bindings are not aggressive - they don't watch an entire object unless they are explicitly asked to (such as with a "deep" watch, which is not advised). As such, they only watch bindings that have been explicitly created.

One way to create a binding is to perform interpolation:

{{ windowTitle.title }}

By including this in your DOM, AngularJS will create a binding for the "Text Node" in the DOM and will evaluate it in the context of the scope during each digest. What's important to understand is that the above digest only checks the *one property* on the windowTitle service. So, even if windowTitle had 1,000 properties, this would not affect the performance as only the *single* property in the interpolation is being checked.

Now, the windowTitle happens to be a single "string" value. If it were, instead, a method call:

{{ windowTitle.generateTitle() }}

.... then things can get hairy. Imagine that .generateTitle() did some intense processing, then suddenly your digest phase is doing a lot of work and you can start to see performance problems.

This is why it is ill-advised to use "filters" in a production AngularJS app. Imagine you have a filter that looks like:

{{ someValue | myFilter }}

Behind the scenes, what that's really doing is evaluating something like:

$scope.myFilter( $scope.someValue )

... and if myFilter() has to do a lot of work (like filter an array or sorting or etc.), then it has to do that on *every* digest and then you can run into performance bottlenecks.

Getting back to the windowTitle example, this should be ok, performance-wise, since there is no "work" required to evaluate the expression - it's just a simple string. The only difference is that it happens to be in an object that lives on the scope, rather than living on the scope directly.

Reply to this Comment

@Atticus,

As to your question as to why this doesn't work:

>> `$scope.windowTitleValue = WindowTitle.title`

The problem here is that ".title" is a String value. This means that it is copied *by value*, not *by reference*. As such, once the reference is assigned, the connection between the two values is broken. Think about it like this (pseudo code):

$scope.windowTitleValue = VALUE_COPY_OF( windowTitle.title )

At that point, no matter what happens to "windowTitle", it will have no affect on the $scope.windowTitleValue (and vice-versa).

Reply to this Comment

@Atticus,

Re: your 3-layers of watch, AngularJS actually added *another* watch method recently: Scope.$watchGroup(). I have not yet played with it yet, though.

Reply to this Comment

@Karol,

Performing this kind of an action inside of a Router *can* work, but it really depends on your situation, and how much work you want to push into the router (and the way it loads data).

In my example, I am setting static values for page title. But, imagine that I was showing a "detail page" in which I had load data from the remote API. Then, once the data was loaded, I used it set the page title:

windowTitle.pushTitle( "Loading..." );
dataService.getByID( 4 ).then( function( response ) {
. . . . windowTitle.replaceTitle( "Viewing: " + response.name );
} );

In this case, I'm actually updating the windowTitle based on the state of the Controller.

Now, again, you *could* do this in the router if you want the router to "Resolve" the data load before the view is rendered. But, that just depends on how much logic you want to be in your router.

Of course, for simple use-cases, doing this is the router would be totally fine.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.