Tricky scope binding in angularjs directives - angularjs

I want to write 'edit in place' directive in angularjs.
I want that directive is reusable, therefore I have following requirements on the directive:
it must be an attirbute that can deocorate any element, that makes sense (div,span,li)
it must support edit button, clicking on that will change set ot displayd elements into input fileds. Typically properties of one object, e.g. contact (number, name)
I disocvere trickery behaviour of scope visibility in the directive that can be seen in this fiddle http://jsfiddle.net/honzajde/ZgNbU/1/.
Comenting out in the directive: template and scope -> contact.number and contact.name are displayed
Comenting out in the directive: scope -> contact.number only is displayed
Not commenting out anything -> nothing is displayed
=> when both are commented out just adding template to the directive makes it render contact.number even though template is not used.
I am asking what are the rules of the game?
<div>
<div ng-controller="ContactsCtrl">
<h2>Contacts</h2>
<br />
<ul>
<li ng-repeat="contact in contacts">
<span edit-in-place="" ng-bind="contact.number"></span> |
<span edit-in-place="" >{{contact.name}}</span>
</li>
</ul>
<br />
<p>Here we repeat the contacts to ensure bindings work:</p>
<br />
<ul>
<li ng-repeat="contact in contacts">
{{contact.number}} | {{contact.name}}
</li>
</ul>
</div>
</div>
var app = angular.module( 'myApp', [] );
app.directive( 'editInPlace', function() {
return {
restrict: 'A',
//scope: { contact:"=" },
template: '<span ng-click="edit()" ng-bind="value"></span><input ng-model="value"></input>',
link: function ( $scope, element, attrs ) {
// Let's get a reference to the input element, as we'll want to reference it.
var inputElement = angular.element( element.children()[1] );
// This directive should have a set class so we can style it.
element.addClass( 'edit-in-place' );
// Initially, we're not editing.
$scope.editing = false;
// ng-click handler to activate edit-in-place
$scope.edit = function () {
$scope.editing = true;
// We control display through a class on the directive itself. See the CSS.
element.addClass( 'active' );
// And we must focus the element.
// `angular.element()` provides a chainable array, like jQuery so to access a native DOM function,
// we have to reference the first element in the array.
inputElement[0].focus();
};
// When we leave the input, we're done editing.
inputElement.prop( 'onblur', function() {
$scope.editing = false;
element.removeClass( 'active' );
});
}
};
});
app.controller('ContactsCtrl', function ( $scope ) {
$scope.contacts = [
{ number: '+25480989333', name: 'sharon'},
{ number: '+42079872232', name: 'steve'}
];
});

You are running into problems because you are misusing angular.
First, a directive should be self-contained, but you are pulling functionality out of it, which makes it less universal and less reusable. In your code, you have functionality in the DOM and in the controller that belongs in the directive. Why?
Second, it's also unclear from your markup and javascript specifically want you want to accomplish when all these pieces are strung together.
Third, in most cases, directives should have their own isolated scope, which is done by declaring a scope object with attributes it should bind. You shouldn't be passing an expression (i.e. {{contact.name}}) inside the directive as it will break the binding and your contact will not be updated when the edit-in-place finishes. The proper way is to establish bi-directional binding through an = property on the scope. ng-bind isn't what you want here: that's scope-specific, so we use it inside the directive's scope. As Valentyn suggested, you could do some magic to get around this, but it's not a good idea and it's super-simple to set it up the right way. What's the issue with doing this by an attribute?
This is all bad Ju-ju.
As I pointed out in your other question on this same topic, you must make your directive self-contained and work with angular, rather than against it. Here's an attribute-based version of the fiddle I gave you previously, meeting the first of your requirements. Please let me know what is wrong specifically with this implementation and we can talk about the angular way of fixing it.
Lastly, if you provide further context on what you need in terms of a "button", I'll incorporate that into the fiddle too.
[update]
It is possible to make the directives work your way, but you will run into problems eventually (or right now, it would seem). All components in an angular app (or any app for that matter) should be as self-contained as is feasible. It's not a "rule" or limitation; it's a "best practice". Similarly, communication between directive components can occur through a controller, but it shouldn't. Ideally, you shouldn't reference the DOM in a controller at all - that's what directives are for.
If your specific purpose is a row that is editable, then that is your directive. It's okay to have a lower-level generic edit-in-place directive that the larger directive uses, but there is still the higher-level directive too. The higher-level directive encapsulates the logic between them. This higher-level component would then require a contact object.
Lastly, no, there isn't necessarily a big difference between ng-bind="var" and {{var}}. But that's not the issue; the issue was where that binding takes place. In your example, a value was passed to the directive instead of a bi-directionally-bound variable. My point was that the directive needs access to the variable so it can change it.
Summary: You are coding in a very jQuery-style way. That's great for coding in jQuery, but it doesn't work so well when coding in Angular. In fact, it causes a lot of problems, like the ones you're experiencing. In jQuery, you would, for example, dynamically insert DOM elements, declare and handle events, and manually bind variables all within a single code block, all manually. In Angular, there is a clean separation of concerns and most of the binding is automatic. In most cases, it leads to javascript code at least two-thirds smaller than the jQuery alternative. This is one of those cases.
That said, I have created a Plunker that contains a more sophisticated version of both the edit-in-place as well as a new higher-level directive to incorporate additional features: http://plnkr.co/edit/LVUIQD?p=preview.
I hope this helps.
[update 2]
These are the answers to your new round of questions. They may be good for your edification, but I already gave you the "angular way" to fix your problem. You will also find that I already addressed these questions (in broader strokes) earlier in my original answer as well as in my update. Hopefully, this makes it more apparent.
Question: "Comenting out in the directive: template and scope -> contact.number and contact.name are displayed"
My Reply: When you do not specify a scope, the directive inherits its parent scope. You bound and interpolated the name and number within the context of the parent, so it "works". Because the directive will alter the value, however, this is not a good way way to solve it. It really should have its own scope.
Question: "Comenting out in the directive: scope -> contact.number only is displayed"
My Reply: You bound a scope property of the parent to the "contact.number" directive, so it will get placed inside during the $digest loop - after the directive has been processed. On the "contact.name", you put it inside the directive, which can only work if the directive codes for transclusion.
Question: "Not commenting out anything -> nothing is displayed"
My Reply: Right. If the directive has its own scope (and this one definitely should), then you must use a defined directive scope property to communicate values, as my several code samples demonstrate. Your code, however, tries to use the parent scope in the directive when we explicitly forbid that by using the scope property in its definition.
Summary: While this second update may be informative (and I hope that it is), it doesn't answer the question beneath your questions: how do I use angular components correctly so that the scope I'm using is always what I think it is? My first post and the subsequent update, answer that question.

Here is little bit updated your fiddle, but it need further improvements to meet full list of your requirements: http://jsfiddle.net/5VRFE/
Key point is:
scope: { value:"=editInPlace" },
Some notes: its better to use ng-show ng-hide directivies for visual appearing-hiding instead of changing css classes. Also its better to spread functionality into different directives to have better separation of concerns (check ngBlur directive)
About your confusion of scope check guide about scopes paragraph "Understanding Transclusion and Scopes": Each directive have separate isolated scopes, if you want to have access from directive's template to controller's scope use directive scope binging ("scope" field of directive definition object). And also transcluded elements have a scope of from where you defined transcluding template.
From the first view those isolated scope sounds little bit strange, but when you have good structured directives (note also that one directive can require another and share bindings) you can find it extremly usefull.

Related

Function never called in an AngularJS directive controller

I am facing a strange situation in which I have created a directive, to which a controller is attached, and one of the two tiny functions of the controller is never called from the view whereas the other function is.
Here is the plunker.
The message I expect is (bold is what does not show up)
You are limited to: Prison
I have already created tens of directives, whether in their own right or as wrappers around existing directives available on GitHub, from lightweight ones such as custom-select to behemoths such as angular-ui-grid.
I am at the end of my wits here as to why {{getArea()}} produces no text at all in the view. I've scrutinized the code, trying to do it with new eyes, so to speak, and I see nothing wrong. I've created a specific project in Eclipse for this tiny piece of code, installed Wampserver just so I could set breakpoints in Firebug and God knows to what great lengths I had to go just so that I could understand what is wrong with the code I wrote.
For instance, in isRestricted(), I can call getArea() without any problem. However, Angular seems to not find the function from the directive.
A few similar questions have already been asked but none of the errors (missing controller or ng-app specification, missing dependency list at module declaration, nested controllers, etc.) seem to apply. There's obviously an important lesson to be learned here and I'm truly eager to learn it.
EDIT: The lesson learned is that ng-if creates a new scope. That new scope comes in between the controller and the directive, which leads to the template of the directive losing access to anything defined in the controller (at least, that's how I would phrase it). (Note that a comment hinted at directive priority.)
There are several solutions, which all maintain the prototypical inheritance needed for the template to access the functions defined in the controller:
not using an isolate scope
not defining the ng-if directive on the top-level element of my directive, as that causes a conflict (between my controller's scope and the scope defined by ng-if). I believe ng-if wins here, which leads to the controller's scope being out of reach of the directive. Using ng-if on a child div does the trick (because then, the ng-if scope inherits my controller's scope, hence making the functions available to the template).
Because of the CSS styling needed with this directive, I have used scope: false.
<span class="scoop-badge-content">{{$parent.getArea()}}</span>
Or in directive :
scope:true
This is because ng-if use how own scope
The strange thing is that when i have this problem i usually use dot notation. But it doesn't work here, probably because we're inside a directive, and i didn't had the case until now.
EDIT : a last way of doing this chaging the template :
<div class="scoop-badge scoop-badge-ua">
<div ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I think this work because you have replace true and ng-if will conflict with ng-scope if it's on the top DOM element.
When you have scope = {} in your directive, Angular creates an isolated scope. Which means it can't get to the getArea() function.
You can completely remove the scope = {} line or set it to scope = true or scope = false depending on what you're trying to achieve later on.
When set to scope = true Angular will create a new scope object and assign it to the directive. This scope object is prototypically inherited from its parent scope.
When set to scope = false the directive will use its parent scope. (This is the default value. It has the same effect if you remove this line).
More information about scopes here
Removing scope: {} from directive definition solves problem.
app.directive('scoopBadgeUa', function() {
return {
restrict : "A",
scope: {}, // This is not needed, creates conflict
templateUrl : "scoop-badge-ua.html",
replace : true,
controller : 'ScoopBadgeUaController',
};
});
Your code is correct, you don't have to do anything more than adding a <div></div> wrapping your code in scoop-badge-ua.html.
<div>
<div class="scoop-badge scoop-badge-ua" ng-class="{'visible': isRestricted()}" ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I like this solution better than transclude: true because it doesn't include the directive's tag in your html code, which ultimately translates to a cleaner code.

Angularjs dynamically added ng-model is not accessible by $scope, shows undefined

I am creating form, where few fields are dynamic, ng-model is added dynamically.
Ex.:
form.append("<input type='hidden' name='paymillToken' value='" + token + "' data-ng-model = 'formdata.token'/>");
This fields shows undefined while I try to access using $scope.formdata.token
Following is another scenario where I am adding fields via ajax.
angular.forEach(data.data, function(obj, key) {
list+='<div class="items text-center"><img src="assets/uploads/discs/'+obj.image+'" class="img-circle"><br><input type="radio" id="chkDisc'+obj.id+'" name="disc_id" value="'+obj.id+'" required data-ng-model="formdata.disc_id" /></div>';
});
$scope.discslist = $sce.trustAsHtml(list);
This model disk_id is not accessible too.
Okay, to expand on my comment and a bit more on what everyone else here is saying to you, the main issue you're having is inherent in your approach. The way you're trying to manipulate the DOM is very un-AngularJS.
In AngularJS, when you want to change what is displayed to the user (the view), you make changes to your model (your controller scope). That means, you have to set up your view to be able to respond to those changes. We do that with directives and expressions in Angular.
You're probably already using directives to respond to changes in your model whether you realize it or not. ngRepeat, ngModel, ngShow, ngIf, ngInclude, are a handful you're probably familiar with, and even forms and form elements like inputs are actually directives in Angular. When you use these, a change in your model (such as loading data into the controller scope) signals to Angular that it should check whether that change affects any of the directives in your view, and if so, respond to it by updating the view.
In order to do this, Angular needs to know which parts of the model are connected to which parts of the view. It makes these connections when it compiles the html elements that are added to the page. This compile process happens automatically when you load an Angular app. After that, it's up to us to tell Angular when to compile html that is added to the page.
More often than not, we do this without even realizing it. For example, when you use the ngView directive, it will compile the template for each route that it loads, so that all of the directives in your template are properly linked with their associated model.
I know this is a long explanation, but there are two very important points here that are essential to learning AngularJS:
To change the view, you change your model and let the directives (and expressions) on your page respond to those changes.
When you add html elements to the page, if you want AngularJS to be able to use them in your view, they must be compiled first. The compile process should be done via a directive (either a built in one or a custom one).
So, how does all of this apply to your question?
First, I'm guessing that you're breaking both rules by trying to manipulate the DOM via a controller. Even if it is possible to use $compile in a controller, using a controller to change the DOM is bad practice and simply wrong to do (read the part in that link to the doc that specifically states: Do not use controllers to: Manipulate DOM...). A good rule to remember when you're learning AngularJS is that the only time you should ever be using JQuery or JQLite inside Angular is when you are creating a custom directive.
Okay, so how do you solve your question? Use a directive. It looks like you've got a case where you're trying to iterate over an object called data and add some inputs that correspond to the data.data property. This sounds like a job for ngRepeat.
The first thing you need to do is add your data to your controller and make sure it is accessible to the view. The easiest way to do this is by injecting $scope into your controller and setting the data on a scope variable. In its simplest form, that might look something like this:
angular.module('MyApp', [])
.controller('MyController', ['$scope', function($scope){
$http.get('/some/url/that/returns/the/data').
success(function(data) {
$scope.data = data;
});
}]);
Now that we have the data somewhere that we can access from the view, we can use it with the ngRepeat directive in our html, something like this:
<div ng-controller="MyController">
<div class="items text-center" ng-repeat="disc in data.data">
<img ng-src="assets/uploads/discs/{{disc.image}}" class="img-circle"><br>
<input type="radio" id="{{'chkDisc' + disc.id}}" name="{{disc.disc_id}}" value="{{disc.disc.id}}" required data-ng-model="formdata[disc.disc_id]" />
</div>
</div>
This is a common issue. By updating the value and not the model angular has no idea that the value in the field has been updated. As the first commenter said updating in this manner is completely unnecessary when using ng-model.

Use ngModel with plain ngController instead of directive?

In my application I would like to preserve the option of using plain controllers for certain sections of code - as opposed to creating directives for one-off things that will never be re-used.
In these cases I often want to publish some data from the controller to be used in the contained section. Now, I am aware that I could simply bind items in the controller's scope, however I'd like to specify the "model" location explicitly just to make the code more maintainable and easier to read. What I'd like to use is ng-model as it would be used on a custom directive, but just along side my plain controller:
<div ng-controller="AppController" ng-model='fooModel'>
{{fooModel}}
</div>
However I can see no way to get a reference to the generated ngModelController without using a directive and the 'require' injection.
I am aware that I could make my own attribute fairly easily by injecting the $attr into my controller and do something like:
<div ng-controller="AppController" my-model='fooModel'>
{{fooModel}}
</div>
In which case I just manually take or parse the myModel value and stick my model into the $scope under that name. However that feels wrong in this case - I really only need one "model" for a controller and I'd prefer not to have to add this boilerplate to every controller when ngModel exists. (It's the principle of the thing!)
My questions are:
1) Is there some way to use ngModel along with a plain controller to get the effect above?
2) I have been trying to figure out where ngModelControllers are stored so that I could look at the situation in the debugger but have not been able to find them. When using an ngModel directive should I see these in the scope or parent scope? (Where do they live?!?)
UPDATE: As suggested in answers below $element.controller() can be used to fetch the controller. This works (http://plnkr.co/edit/bZzdLpacmAyKy239tNAO?p=preview) However it's a bit unsatisfying as it requires using $evalAsync.
2) I have been trying to figure out where ngModelControllers are stored so that I could look at the situation in the debugger but have not been able to find them. When using an ngModel directive should I see these in the scope or parent scope? (Where do they live?!?)
The answer depends slightly on where you want to access the controller from.
From outside the element with ng-model
It requires "name" attributes on both the element with the ng-model attribute, and a parent form (or ngForm). So say you have the form with name myForm and the element with ng-model attribute with name myInput, then you can access the ngModelController for myFoo from the parent scope as myForm.myInput. For example, for debugging purposes:
<p>myFoo: {{myForm.myInput.$modelValue}}<p>
<form name="myForm">
<div ng-controller="InnerController" name="myInput" ng-model="model.foo"></div>
</form>
as can be seen at http://plnkr.co/edit/IVTtvIXlBWXGytOEHYbn?p=preview
From inside the element with ng-model
Similar to the answer from #pixelbits, using $evalAsync is needed due to the order of controller creation, but you can alternatively use angular.element.controller function to retrieve it:
app.controller('InnerController', function($scope, $element) {
$scope.$evalAsync(function() {
$scope.myModelController = $element.controller('ngModel');
});
});
Used, inside the controller to view it, for debugging purposes, as:
<div ng-controller="InnerController" ng-model="model.foo">
<p>myFoo: {{myModelController.$modelValue}}<p>
</div>
As can be seen at http://plnkr.co/edit/C7ykMHmd8Be1N1Gl1Auc?p=preview .
1) Is there some way to use ngModel along with a plain controller to get the effect above?
Once you have the ngModelController inside the directive, you can change its value just as you would were you using a custom directive accessing the ngModelController, using the $setViewValue function:
myModelController.$setViewValue('my-new-model-value');
You can do this, for example, in response to a user action that triggers an ngChange handler.
app.controller('InnerController', function($scope, $element) {
$scope.$evalAsync(function() {
$scope.myModelController = $element.controller('ngModel');
});
$scope.$watch('myModelController.$modelValue', function(externalModel) {
$scope.localModel = externalModel;
});
$scope.changed = function() {
$scope.myModelController.$setViewValue($scope.localModel);
};
});
Note the extra watcher on $modelValue to get the initial value of the model, as well as to react to any later changes.
It can be used with a template like:
{{model.foo}}
<div ng-controller="InnerController" ng-model="model.foo">
<p><input type="text" ng-model="localModel" ng-change="changed()"></p>
</div>
Note that this uses ngChange rather than a watcher on localModel. This is deliberate so that $setViewValue is only called when the user has interacted with the element, and not in response to changes to the model from the parent scope.
This can be seen at http://plnkr.co/edit/uknixs6RhXtrqK4ZWLuC?p=preview
Edit: If you would like to avoid $evalAsync, you can use a watcher instead.
$scope.$watch(function() {
return $element.controller('ngModel');
}, function(ngModelController) {
$scope.myModelController = ngModelController;
});
as seen at http://plnkr.co/edit/gJonpzLoVsgc8zB6tsZ1?p=preview
As a side-note, so far I seem to have avoided nesting plain controllers like this. I think if a certain part of the template's role is to control a variable by ngModel, it is a prime candidate for writing a small directive, often with an isolated scope to ensure there are no unexpected effects due to scope inheritance, that has a clear API, and uses require to access the ngModelController. Yes, it might not be reused, but it does help enforce a separation of responsibilities between parts of the code.
When you declare directives on an element:
<div ng-controller="AppController" ng-model='fooModel'>
{{fooModel}}
</div>
You can retrieve the controller instance for any directive by calling jQlite/jQuery $element.data(nameOfController), where nameOfController is the normalized name of the directive with a $ prefix, and a Controller suffix.
For example, to retrieve the controller instance for the ngModel directive you can do:
var ngModelController = $element.data('$ngModelController');
This works as long as the ngModel directive has already been registered.
Unfortunately, ngController executes with the same priority as ngModel, and for reasons that are implementation specific, ngModel is not registered by the time that the ngController function executes. For this reason, the following does not work:
app.controller('ctrl', function ($scope, $element) {
var ngModelController = $element.data('$ngModelController');
// this alerts undefined because ngModel has not been registered yet
alert(ngModelController);
});
To fix this, you can wrap the code within $scope.$evalAsync, which guarantees that the directives have been registered before the callback function is executed:
app.controller('ctrl', function ($scope, $element) {
$scope.$evalAsync(function() {
var ngModelController = $element.data('$ngModelController');
alert(ngModelController);
});
});
Demo JSFiddle

Access to form controller hidden due to angular ui tab isolated/inherited scope

I have a simple case:
<div ng-controller="myController">
<tabset>
<tab>
<form name="myForm"></form>
</tab>
</tabset>
</div>
and now, in myController method, I would like to access myForm to call $setPristine:
$scope.myForm.$setPristine()
but I can not. tabset / tab creates isolated/inherited scope. It's just a sample, but I run into this problems when using angular js directives that create isolated scopes many times.
How can I get over this issue? In the past I did something like this (with ng-table that also creates new scope):
ng-init="$parent.saataaTable = this"
but it's far from perfect.
This was one of the most difficult concepts for me to get around and my solution is simple but kind of difficult to explain so bear with me.
Solution 1: Isolate Scopes
When you are only dealing with only isolate scopes (scope: {...}) or no scope (scope: false), you're in luck because the myForm will eventually be there. You just have to watch for it.
$scope.$watch('myForm', function(val) {
if (myForm) {
// now I can call $setPristine
}
});
Solution 2: Child Scopes
This is when you set scope: true or transclude: true. Unless you perform a custom/manual transclusion you will not get myForm on the controller's scope.
The trick is to access the form's controller directly from the form element. This can be done by the following:
// at the form element
element.data('$formController');
// or at the control (input, select, etc.)
element.inheritedData('$formController');
// where 'element' is a jqLite element (form or ng-form)
This sets you up for a new issue: how do we know when and how we can get that element and it's data.
A quick answer is that you need to set up a dummy $watch on your controller's scope to look for (in your case) myForm. When this watch is processed you will then be able to attempt to locate the form. This is necessary due to the fact that typically when your controller first executes the FormController won't yet be on the element's data object.
A quick and simple way to find the form is to simply get all of the forms. NOTE: if there are multiple forms within the element you'll have to add some logic to find the right one. In this case our form is a form element and it's the only one. So, locating it is fairly easy:
// assuming you have inject $element into your controller
$element.find('form').data('$formController');
// where $element is the root element the controller is attached to
// it is injected just like '$scope'
Once you have the controller you can access everything you would normally. It is also important to note that Solution 2 will always work once that FormController is on the element.
I have set up a Plunk to demonstrate the code here, but please note that is a demonstration so not all best practices were kept in mind.
EDIT
I found it important to note that if you don't want to worry about the scopes of the nested directives you can just watch the form name on the scope and handle things there.
$scope.$watch('myForm', function(val) {
if (angular.isDefined(val)) {
// now I have access
} else {
// see if i can `find` the form whose name is 'myForm'
// (this is easy if it is a form element and there's only one)
// then get the FormController for access
}
}
I could not make it work using the answer above, but I found a work-around.
In the form, I created a hidden input field with a ng-model and ng-init that set its value to the form. Then in my submit function in the controller I can access the formController via this ng-model
So, in the HTML, I create a hidden field inside the form:
<input id="test" ng-model="data.myForm" ng-init="data.myForm=myForm" hidden>
And in the Controller I can get hold of the formController via data.myForm
$scope.data.myForm.$setPristine();
It is probably not very good, so I will instead avoid to rely on the $pristine and $dirty properties of the formController and find another way to detect if the form has changed (using a master copy of the object, like they do in the sample in the documentation)

AngularJS, bind scope of a switch-case?

To get a grip on AngularJS I decided to play around with one of the examples, specifically, simply adding a "complete" screen to the Todo-example, when the user has entered 5 todos it uses a switch-case to switch to another div. Code is available here http://jsfiddle.net/FWCHU/1/ if it's of any use.
However, it appears that each switch-case gets its own scope ($scope.todoText is not available), however it can be accessed using "this" from within addTodo() in this case. So far so good, but say I want to access todoText (which is inside the switch-case) outside of the switch-case, how would I go about doing that? Can I bind the switch-case scope to the model perhaps, is it accessible in some other way or should this be solved in some other way?
PS. I'm not trying to find ANY solution to the code above, I'm pretty sure I know how to solve it without using switch-cases, I want to understand how scopes work in this case!
Mark has some great suggestions! Make sure you also check out the AngularJS Batarang Chrome Extension to see the various scopes and their values (among other things). Note it doesn't appear to work well with jsFiddle.
I'm not sure how to access inner scopes directly but here is one way to access the same text in the outer scope by binding to an object instead of a primitive.
1) Declare todoText as an object instead of a primitive in your controller:
$scope.todoText = {text: ''};
2) Bind to todoText.text instead of just todoText:
<form ng-submit="addTodo()">
<input type="text" ng-model="todoText.text" size="30" placeholder="add new todo here">
<input class="btn-primary" type="submit" value="add">
</form>
3) Modify the existing functions to use todoText.text:
$scope.addTodo = function() {
$scope.todos.push({text:$scope.todoText.text, done:false, width: Math.floor(Math.random() * 100) + 50});
$scope.todoText.text = '';
};
Take a look at this fiddle and note that the text displayed beneath the textbox when you type something in is accessing the todoText.text on the outside scope.
If you change the code back to use a primitive (like in this fiddle) the parent scope todoText won't reflect any changes you make to the textbox. This is likely more to do with how JavaScript copies reference values (see this post for more info) and less an AngularJS specific thing.
Update2: Now that I know a little more about AngularJS, here's a much better answer.
say I want to access todoText (which is inside the switch-case)
outside of the switch-case, how would I go about doing that?
There is no way for parent scopes to access child scopes. (One reason for this restriction, according to Angular developers, is for easier memory management of scopes.) (Well, you could use $$childHead and $$childTail to access child scope, but you shouldn't!)
Can I bind the switch-case scope to the model perhaps, is it
accessible in some other way or should this be solved in some other
way?
There are three common ways to access the parent model from the child scope:
Do what #Gloopy suggests: create an object in the parent scope, then refer to properties on that object in the child scope.
Use $parent in the child scope to access the parent scope and its properties, even primitive properties.
Call a method on the parent scope
To convert your fiddle to use $parent:
<input type="text" ng-model="$parent.todoText" ...
$scope.addTodo = function() {
$scope.todos.push({text: $scope.todoText, ...
$scope.todoText = '';
As I mentioned in the comments on Gloopy's answer, ng-repeat and ng-switch both have the new child scope prototypically inherit from the parent scope. ng-repeat also copies the loop variable/item to the new child scope (and the nuances that #Gloopy describes with primitives vs object applies). ng-switch does not copy anything from the parent scope.
To see what the inner/child scope looks like, add the following after the ng-switch-when:
<a ng-click="showScope($event)">show scope</a>
and add this to your controller:
$scope.showScope = function(e) {
console.log(angular.element(e.srcElement).scope());
}
Update1: (strikethroughs added to bad advice, []'s added for clarity)
For this scenario, where AngularJS is creating additional inner scopes (implicitly), and you don't really want/need another controller, I like Gloopy's solution. A service (what I originally suggested below) is [the wrong way to do this] probably overkill here. I also like that Gloopy's solution does not require the use of 'this' in the controller methods.
Original Answer: (strikethroughs added to bad advice, []'s added for clarity)
To see where scopes are being created (if you haven't tried this already, it is handy):
.ng-scope { margin: 4px; border: 1px dashed red }
To access todoText outside the switch-case (hence outside of its scope), you're essentially asking about inter-controller communication, since multiple scopes are involved. You have a few options, but a service is probably best. Store the data (that needs to be shared) inside the service, and inject that service into each controller that needs access to the data.
For your specific example, I think you'd need to attach a controller to each switch-case and inject the service into it, to get access to the shared data.
See also AngularJS: How can I pass variables between controllers?.
The other options:
Using $scope.$parent in the inner scope is [one way to do this -- see Update2 above] not recommended, since then a controller would be making assumptions about how the data is presented.
Using $rootScope is not recommended, except maybe for simple, one-off applications. That shared data may start to take on a life of its own, and $rootScope is not the place for that to happen. Services are easier to reuse, to add behavior to, etc.
Using $scope.$emit is another option, but it seems messy and a bit odd: emitting events to share data (instead of triggering behavior).
[Using an object in the parent scope is probably best -- see #Gloopy's answer.]

Resources