Q.: Why does ng-switch create a new scope?

6,816 views
Skip to first unread message

Godmar Back

unread,
Aug 24, 2012, 12:19:18 AM8/24/12
to ang...@googlegroups.com

Hi,

To my knowledge, control-flow statements don't create new scopes in any widely used programming language. (In many C-like languages (but not JavaScript), the programmer can decide if they want a new scope using {}, but it's not automatic. And, of course, those languages don't use prototypical inheritance in which the value of an assigned value is inaccessible in the parent scope!)

In Angular, ng-switch creates a new scope, which means that variables set in a ng-switch constructs aren't accessible outside of it.

This makes coding difficult and brittle.  For instance, consider this fragment for a collapse/expand logic for a tree; also at http://jsfiddle.net/LAYGE/1/:

<script type="text/ng-template" id="simpletemplateworks">
<li ng-init="collapsed=true">
  <div ng-click="collapsed = !collapsed" 
    <span ng-show="collapsed">Click to Expand</span>
    <span ng-hide="collapsed">Click to Collapse</span>
  </div>
  <ul ng-hide="collapsed">
     <ng-include src="'simpletemplateworks'" ng-repeat="entry in entry.children"></ng-include>
  </ul>
</li>
</script>

Simple enough. Now suppose I want to suppress the 'Click to Expand' message for Leaf nodes, so I might naively do:

<script type="text/ng-template" id="morecomplextemplate">
  <li ng-init="collapsed=true">
     <ng-switch on="entry.children.length > 0">
            <div ng-switch-when="true" 
              ng-click="collapsed = !collapsed" 
                  <span ng-show="collapsed">Click to Expand</span>
                  <span ng-hide="collapsed">Click to Collapse</span>
            </div>
            <div ng-switch-when="false">Leaf</div>
     </ng-switch>

     <ul ng-hide="collapsed">
      <ng-include src="'morecomplextemplate'" ng-repeat="entry in entry.children"></ng-include>
     </ul>
  </li>
</script>

All the sudden, it's broken.  'collapsed' inside ng-switch no longer refers to the 'collapsed' variable controlling the visibility of <ul>!

Now suppose the programmer is aware of this unexpected design choice - how would they fix it robustly?  The solution I came up with was to add '$parent.collapsed = collapsed'

<script type="text/ng-template" id="morecomplextemplate">
  <li ng-init="collapsed=true">
        <ng-switch on="entry.children.length > 0">
           <!-- ng-switch creates a new scope.  Hence we must use $parent -->
           <div ng-switch-when="true" 
              ng-click="collapsed = !collapsed; $parent.collapsed = collapsed" 
                  <span ng-show="collapsed">Click to Expand</span>
                  <span ng-hide="collapsed">Click to Collapse</span>
            </div>
                  <div ng-switch-when="false">Leaf</div>
        </ng-switch>

     <ul ng-hide="collapsed">
      <ng-include src="'morecomplextemplate'" ng-repeat="entry in entry.children"></ng-include>
     </ul>
  </li>
</script>

Ok - but what if the template gets more complicated?  Say I need additional levels of ng-switch statements? I will have to count out, manually, how many scopes the position where 'collapsed' is set is from where it is used. IMO, this makes for brittle and difficult to read code.

I'm curious to learn where the choice to create a new scope for switch is advantageous and what moved Angular's designer to make this choice. 

 - Godmar

Olivier Clément

unread,
Oct 1, 2012, 1:35:27 PM10/1/12
to ang...@googlegroups.com
Nobody have anything to say on this?
I'm learning Angular right now and would be curious to have some more details on all this.
Reading that post, it does sound like an issue to me

To OP, did you created an issue on Angular's GitHub?

Witold Szczerba

unread,
Oct 1, 2012, 7:06:23 PM10/1/12
to ang...@googlegroups.com
Hi,
you cannot compare if/else statements in programming languages with
angular directives and templating system. The term "scope" refers to
totally different things.
To answer your question - the basic idea is that templates (maybe
excluding trivial ones) are not supposed to contain logic, it should
be coded in controllers and referenced by templates. That way you are
not going to have the issues like you have described, because child
scopes inherit from parent ones.

Additionally - logic in controllers lets you codify specifications in
unit tests and watch them being accepted or rejected while programming
(each JS file save can give you an instant feedback). Of course,
controllers can go through all the tests, but forms can be mis-wired,
so basic e2e tests (manual or automatic) can help to figure it out.

Regards,
Witold Szczerba

P.S.
Do not write code in e-mail, publish it through services like
jsfiddle. It makes it easy to publish modifications, fix bugs and what
else.
> --
> You received this message because you are subscribed to the Google Groups
> "AngularJS" group.
> To post to this group, send email to ang...@googlegroups.com.
> To unsubscribe from this group, send email to
> angular+u...@googlegroups.com.
> Visit this group at http://groups.google.com/group/angular?hl=en.
>
>

Godmar Back

unread,
Oct 1, 2012, 9:02:30 PM10/1/12
to ang...@googlegroups.com
On Mon, Oct 1, 2012 at 7:06 PM, Witold Szczerba <pljos...@gmail.com> wrote:
Hi,
you cannot compare if/else statements in programming languages with
angular directives and templating system. The term "scope" refers to
totally different things.

A scope is a context that contains a set of bindings from names to values. In programming languages as in Angular.
 
To answer your question - the basic idea is that templates (maybe
excluding trivial ones) are not supposed to contain logic, it should
be coded in controllers and referenced by templates.

The example I gave was a trivial one. Moving variables such as 'collapsed', 'expanded', 'visible' to the JavaScript side would pollute your controller logic, which (IMO) should have to deal only with the actual model as far as it relates to the application (ideally). These variables, though technically part of Angular's 'model', represent purely indicators of the current view (for instance, is something shown/hidden, collapsed/expanded).  For instance, in a traditional web application, they might be ephemeral state and you normally wouldn't store them on the server.
 
That way you are
not going to have the issues like you have described, because child
scopes inherit from parent ones.


The inheritance isn't causing the issue. It's the fact that assignments in the child scope do not propagate to the parent that can cause problems if the creation of a new scope is not apparent.


Do not write code in e-mail, publish it through services like
jsfiddle. It makes it easy to publish modifications, fix bugs and what
else.

Yes, please show a version of my jsfiddle that would use your advice, then we can compare clarity and testability.

 - Godmar

Renan T. Fernandes

unread,
Oct 1, 2012, 9:47:01 PM10/1/12
to ang...@googlegroups.com
golden rule: use objects instead of putting primitive types in the base of scope
in the parent controller you simply create the object and in the child you update it with what you want.


2012/10/1 Godmar Back <god...@gmail.com>

--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To post to this group, send email to ang...@googlegroups.com.
To unsubscribe from this group, send email to angular+u...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular?hl=en.
 
 



--
Renan Fernandes(aka "ShadowBelmolve")

Renan T. Fernandes

unread,
Oct 1, 2012, 9:50:04 PM10/1/12
to ang...@googlegroups.com
see if this is what you want http://jsfiddle.net/LAYGE/6/

2012/10/1 Renan T. Fernandes <re...@kauamanga.com.br>

Witold Szczerba

unread,
Oct 2, 2012, 5:14:00 AM10/2/12
to ang...@googlegroups.com
On 2 October 2012 03:02, Godmar Back <god...@gmail.com> wrote:
> On Mon, Oct 1, 2012 at 7:06 PM, Witold Szczerba <pljos...@gmail.com>
>> To answer your question - the basic idea is that templates (maybe
>> excluding trivial ones) are not supposed to contain logic, it should
>> be coded in controllers and referenced by templates.
>
> The example I gave was a trivial one. Moving variables such as 'collapsed',
> 'expanded', 'visible' to the JavaScript side would pollute your controller
> logic, which (IMO) should have to deal only with the actual model as far as
> it relates to the application (ideally). These variables, though technically
> part of Angular's 'model', represent purely indicators of the current view
> (for instance, is something shown/hidden, collapsed/expanded). For
> instance, in a traditional web application, they might be ephemeral state
> and you normally wouldn't store them on the server.

As you said, your example was trivial, there was no need to put such a
simple logic inside controller. Sometimes presentation logic can
become more and more complicated and one day it makes everything
easier to move that logic to controller.

> The inheritance isn't causing the issue. It's the fact that assignments in
> the child scope do not propagate to the parent that can cause problems if
> the creation of a new scope is not apparent.

In your very case the inheritance was actually causing an issue. In
child scopes you were re-assigning values. This is how inheritance in
JavaScript works, it is not AngularJS invention. That is, however, not
a big deal, because - as Renan said, you can easily immune to such a
problems by introducing an intermediate object. In his version of your
jsfiddle the object was called "data", so you never reassign anything
on scope but on "data" instead. Scope inheritance gives you access to
the same instance of "data" everywhere on a form.

Regards,
Witold Szczerba

Godmar Back

unread,
Oct 2, 2012, 7:42:49 AM10/2/12
to ang...@googlegroups.com
On Tue, Oct 2, 2012 at 5:14 AM, Witold Szczerba <pljos...@gmail.com> wrote:

> The inheritance isn't causing the issue. It's the fact that assignments in
> the child scope do not propagate to the parent that can cause problems if
> the creation of a new scope is not apparent.

In your very case the inheritance was actually causing an issue. In
child scopes you were re-assigning values. This is how inheritance in
JavaScript works, it is not AngularJS invention. That is, however, not
a big deal, because - as Renan said, you can easily immune to such a
problems by introducing an intermediate object. In his version of your
jsfiddle the object was called "data", so you never reassign anything
on scope but on "data" instead. Scope inheritance gives you access to
the same instance of "data" everywhere on a form.


Witold, you completely missed my point.

Prototypical inheritance is not God given, it's a design choice. Angular didn't have to adopt prototypal inheritance for its (lexical) scopes. Especially considering that not even JavaScript uses it for its lexical scoping. 

The only reason I could imagine is that they simply mapped their scope implementation to prototype chains and so didn't have to implement their own bindings. But given that they implemented their own expression compiler, this doesn't sound compelling. So the questions of 'why' remains.

 - Godmar

Peter Bacon Darwin

unread,
Oct 2, 2012, 8:07:21 AM10/2/12
to ang...@googlegroups.com
There is actually no need for ng-switch to create a new scope and I would argue that in fact it shouldn't.  Check out this plunk where I have hacked the Angular.js file so that ng-include just simply uses the scope of the directive for the scope of the children: http://plnkr.co/edit/yKXYts?p=preview.  The lines of the angular.js file that have changed (I kept the originals as comments) are 13522, 13528, 13529.
Pete

Witold Szczerba

unread,
Oct 2, 2012, 9:06:15 AM10/2/12
to ang...@googlegroups.com
I think they root cause for directives like ngSwitch creating new
scopes, was to avoid memory and listener leaks. It's easier to track a
scope and destroy it with all the stuff attached than tracking
independently all the bindings and whet-else attached to scope. Check
if your version of ngSwitch does not cause leaks. I have not browse
your changes, it would be simpler if you used tools like github gist
or fork to show file modifications.
It was long time ago when I was figuring out the reason for scope
hierarchies and creation, so everything I have just wrote may be wrong
:)

Regards,
Witold Szczerba

Misko Hevery

unread,
Oct 3, 2012, 1:55:08 AM10/3/12
to ang...@googlegroups.com
The reason why ng-switch creates a new scope, is that scope is used for memory management. When the switch branch is removed we need to remove all of the bindings as well as all of the click listeners. This is all done through scope.

To put it differently whenever DOM structure changes, a new scope needs to be created, so that we can properly clean up after ourselves.

-- Misko

Peter Bacon Darwin

unread,
Oct 3, 2012, 4:17:39 AM10/3/12
to ang...@googlegroups.com
Hi Misko

It is great to see you on the mailing list again.  I thought this one (and the DI question earlier) would entice you in!

I get it that the case directives tranclusion can add $watches to the scope and so it is important to tidy up, in the same way that the element.remove tidies up and jQuery handlers.
What else can the transclusion add to the scope that needs to be tidied up?
What happens if the transclusion adds a watch to the $parent scope?

Does the switch directive have a well known unchanging number of case directives below it at link time?  I.E. I guess we can't add new case directives on the fly - or can we using ng-repeat say?
If this is the case, isn't there some way that we can create each of the transclusions once and then cache them rather than destroying and recreating them each time?  If so then we wouldn't need a new scope each time?  Is this correct?

Pete

Misko Hevery

unread,
Oct 4, 2012, 12:37:27 PM10/4/12
to ang...@googlegroups.com
Hi Pete

On Wed, Oct 3, 2012 at 1:17 AM, Peter Bacon Darwin <pe...@bacondarwin.com> wrote:
Hi Misko

It is great to see you on the mailing list again.  I thought this one (and the DI question earlier) would entice you in!

thanks
 
I get it that the case directives tranclusion can add $watches to the scope and so it is important to tidy up, in the same way that the element.remove tidies up and jQuery handlers.
What else can the transclusion add to the scope that needs to be tidied up?
It is not just transclusion, but the component itself. We need to clean up all of the $watch-es on this as well as child scope.
 
What happens if the transclusion adds a watch to the $parent scope?

see above
 
Does the switch directive have a well known unchanging number of case directives below it at link time?  

switch has fixed number of cases. You could write your own which would behave differently
 
I.E. I guess we can't add new case directives on the fly - or can we using ng-repeat say?
ng-repeat needs to be homogeneous, so i don't think it is what you want, but ng-include allows you to basically act as as switch and have any number of types.
 
If this is the case, isn't there some way that we can create each of the transclusions once and then cache them rather than destroying and recreating them each time?  If so then we wouldn't need a new scope each time?  Is this correct?


this is complex, since you would have to disconnect the tree without cleaning up associated data, and then prevent $digest propagation into that branch, and then re-attach it all back. Child directives would be confused, because from their point of view they have never gone away.

Peter Bacon Darwin

unread,
Oct 4, 2012, 4:03:25 PM10/4/12
to ang...@googlegroups.com
Thanks Misko for explaining all that.  So the general rule of thumb is to steer clear of binding straight to values (i.e. strings and numbers) but ensure that there is always at least one level of redirection in one's binding expressions to ensure that any child scope doesn't block changes propagating up the prototype chain.
Pete

Misko Hevery

unread,
Oct 4, 2012, 4:37:31 PM10/4/12
to ang...@googlegroups.com
correct

andrew...@gmail.com

unread,
Nov 26, 2012, 3:39:10 PM11/26/12
to ang...@googlegroups.com
Hi Peter, 

Sorry to resurface this thread, I came across it when I encountered same issue. I was wondering if you could clarify your statement about having at least one level of indirection in your binding expressions. I wasn't quite sure how it applied to the case of the ng-switch. 

Here is a trivial plunker if you had a chance to take a look: http://plnkr.co/edit/lOe6Uy

Thanks, 
Andrew

Peter Bacon Darwin

unread,
Nov 27, 2012, 5:13:22 AM11/27/12
to ang...@googlegroups.com
If you set a field on a scope - it only sets it on that scope and not on the parent scope.

So anything like scope.field = ... will only set the value on that scope, whatever you set field to, even if the parent scope has a field called field.

Because scopes prototypically inherit from their parent you get a different behaviour when you read a field on a scope.

So anything like x = scope.field, will first try to read off the current scope and then look up the prototype chain for the field.

This is how Javascript prototypical inheritance works, it is not special to AngularJS.

Pete


Kyle K

unread,
Sep 24, 2013, 8:24:20 PM9/24/13
to ang...@googlegroups.com, god...@gmail.com
Sorry to dig this up again, but this actually bit me pretty hard and is quite unintuitive.

If you have a containing div with some custom directives that have isolate scopes, two-way binding is broken if one of your directives participates in the ng-switch.

ex:

<div ng-controller='MyController'>

    <div ng-switch='controllerState'>

        <myDirectiveA directiveState='controllerState'></myDirectiveA>

        <div ng-switch-when='a'> Some stuff </div>

        <myDirectiveB ng-switch-when='b' directiveState='controllerState'></myDirectiveB>
    </div>
<div>

Assume DirectiveA and DirectiveB are different directives, but have similar isolate scopes:

scope: {
    directiveState: '='
}

Also assume we're doing some kind of toggling behavior where click-handlers on A and B update directiveState/controllerState and DirectiveB is hidden to begin with.

$scope.$parent in DirectiveA will be the scope of MyController and two-way binding will work as expected.

However, $scope.$parent in DirectiveB, once it is shown via ng-switch, is not the scope of MyController. 

You can do $scope.directiveState = 'xxx' in B and it will not be reflected in the Controller's scope.
Reply all
Reply to author
Forward
0 new messages