I apologize in advance if i'm not wording this properly. I have a textbox with ng-model inside an ng-repeat and when I try to get the textbox value it's always undefined. I just want it to display whatever I type in the corresponding textbox.
It seems to be an issue with the $scope, so how would I make the $scope.postText global or at the controller root level so it can be accessible?
Here's the JSFiddle to help clear things up: http://jsfiddle.net/stevenng/9mx9B/14/
As #Gloopy already stated, ng-repeat creates a new child scope for each item in your posts array. Since each item of the posts array is a primitive (a string), ng-repeat also creates a post property on each child scope, and assigns each of them the appropriate value from the array. Inside the ng-repeat block is ng-model="postText". This creates a postText property on each of the child scopes. Here is what that all looks like (for 2 of the 4 child scopes):
When a user types some text into one of the input textboxes, the appropriate gray box will store the text. (E.g., the 2nd (from the top) gray box will store text a user types into the "tech" textbox.) The parent scope cannot see the postText properties in the child scope -- this is the problem you had. There are three common solutions:
#Gloopy's answer: define a function on the parent scope (which the child scopes can access, because ng-repeat child scopes prototypically inherit from the parent scope) and pass the child scope property value (i.e., postText's value) up to the parent.
Use objects instead of primitives in your posts array. E.g.,
$scope.posts = [ {type: 'tech'}, {type: 'news'}, ...];Then in your ng-repeat loop, use
<input type="text" ng-model="post.postText">
Because each array item is an object (and not a primitive), each child scope gets a reference to the appropriate object in array posts, rather than a copy (of a value). Therefore, post.postText gets created on parent $scope property posts, and hence it is visible to the parent scope. (So, in this case the child scopes would simply call savePost() -- there would be no need to pass any values up to the parent scope.)
In other words, if a user typed "this is tech related" into the first text box, the posts array in the parent would be automatically updated as follows:
$scope.posts = [ {type: 'tech', postText: 'this is tech related'}, {type: 'news'}, ...];One last note: the postText property is not added to the posts object until the user types something.
Use ng-model="$parent.someProperty" to bind the form element to a property on the parent scope, rather than on the child scope. This solution would be difficult to implement for your scenario, and it is a rather fragile solution, as it depends on the HTML structure for scope inheritance... but I mention it for completeness.
(A fourth solution was presented by #Renan in comments on #Gloopy's answer. This is a like solution 1., but with a variation: this is used instead of passing a value up to the parent. I'm not a fan of this approach as it makes it difficult to determine which $scope is being accessed or modified. I think it is better that functions defined on $scope only access and modify their own $scope.)
For more information (and lots more pictures) about how prototypal scope inheritance works in Angular, see What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
In your click expression you can reference the postText and access it in your savePost function. If this wasn't in an ng-repeat you could access the single $scope.postText successfully but ng-repeat creates a new scope for each item.
Here is an updated fiddle.
<div ng-repeat="post in posts">
<strong>{{post}}</strong>
<input type="text" ng-model="postText">
save post
</div>
$scope.savePost = function(post){
alert('post stuff in textbox: ' + post);
}
This may be a late answer. Please refer this fiddle. http://jsfiddle.net/X5gd2/
Please refer to the firebug's console, when u click on the links after typing some texts in the text box. The idea is to have a itemcontroller for each of the view that is repeated inside the ng-repeat.
The item controller:
function postItemController($scope){
$scope.savePost = function(){
console.log($scope.postText + " which belongs to " + $scope.post +" will be saved")
}
}
Split the model up into heading and value
angular.module('MyApp',[]);
function PostCtrl($scope) {
$scope.posts = [{heading:'tech',value:''}, {heading:'news',value:''}, {heading:'sports',value:''},{heading:'health',value:''}];
$scope.savePost = function(post){
alert( 'post stuff in textbox: ' + post);
}
}
HTML below..
<div ng-app="MyApp">
<div ng-controller="PostCtrl">
<div ng-repeat="post in posts">
<strong>{{post.heading}}</strong>
<input type="text" ng-model="post.value">
save post
</div>
</div>
</div>
Check out this Fiddle..
Related
I have an select field with an ng-model attribute in a tab within ui-bootstrap's tabset. There is also a button on the tab. On click of the button I would like to get the selected value in the model. I tried using
<select ... ng-model="selectedOption"></select>
<button ng-click="buttonClick()">Click</button>
and then in the controller
$scope.buttonClick = function() {
//try to access $scope.selectedOption
}
But that does not seem to contain the value. I tried looking into the $scope variable and it seems to contain the selectedOption inside something called $$childTail
Is there any other way to access the selectedOption value or should I change the structure of my view?
I have a Plunker here of what I'm trying to do.
Both Mathew Berg's and Bumblebee Man's answers should work but they do not explain what's happening.
Okay basically it goes down like this:
The tabset directive that you are using does transclusion so the selectedOption is actually in an inner scope(tabset's scope), not in your TabsDemoCtrl's cope. This answer explains what transclusion does, and how you can access an transcluded model.
Transclusion makes shallow copies of your models, so even if you init selected option like $scope.selectedOption = "5"; in your TabsDemoCtrl, your select will show 5 as selected at the begining but changes will not be reflected to your TabsDemoCtrl controller's scope because in the tabset's child scope 5 is just copied, and the one being updated is in the tabset's scope not your scope.
You can also use an object for your select's model. Since the transclusion creates a shallow copy, objects/arrays/functions are copied by reference and variables are copied by value. This is also why you can access your TabsDemoCtrl's button click from the inner scope.
updated plnkr with another alternative solution(the object version) :
created an object in your controller
$scope.selection = {};
and used in model like this
ng-model="selection.option"
http://plnkr.co/edit/5sI0arorV9dULzaDxRra?p=preview
Angular ui tab/tabset directive create new scopes, so in order to access the parent scope (What you're trying to do) just attach $parent
<select name="selectedCampaign" ng-model="$parent.selectedOption" ng-options="option for option in options"></select>
Updated plunkr: http://plnkr.co/edit/BgMMn4tJRWm72ZXeQG3N
Edit: Apologies, I misread the question, can't you just do this?
<select ... ng-model="selectedOption"></select>
<button ng-click="buttonClick(selectedOption)">Click</button>
$scope.buttonClick = function(selectedOption) {
selectedOption.whatever...
}
I have read in different places that it is important that you always use objects in your scope but I haven't found a definitive answer as to why that is. Can someone please help me?
I like Ryan Q's answer but after doing some more research I wanted to add an answer with a little more emphasis on javascript's prototypal inheritance.
The issue here is an object vs. primitive issue (pass by reference and pass by value) and how Javascript’s prototypal inheritance works.
When a javascript class inherits from a parent class, it copies the values over into the child class. There are two types of values that could be copied over, objects or primitives.
When an object is copied in through inheritance, it is passed by reference. This means that any updates I make in my child object will be seen in the parent object as well.
When a primitive is copied in through inheritance, it is passed by value. This means that any updates will NOT be seen in the parent class.
What does this have to do with Angular scopes? When we create a directive, a scope will be created for this directive and we can declare it to be an isolated scope or an inherited scope. If it is an inherited scope, it will inherit its parent’s scope items. Now, if I have primitive values in my parent scope, I will be inheriting them into my child scope as pass by value. Which means when I make a change in my parent scope, it won’t be seen in my child scope, and vice versa. Now I could have the same inherited variable in my child and parent scope with different values. This is going to lead to confusion…. And probably anger…lol.
So if you just use an object, then this problem will not happen. And that is why you should always objects in scopes.
Take a look at this:
https://github.com/angular/angular.js/wiki/Understanding-Scopes
and
Does my ng-model really need to have a dot to avoid child $scope problems?
Lets say you have 3 controllers, a main control, and two others which inherit from the main control.
Your html might look like this.
<div ng-controller="MainCtrl">
<div ng-show="helloWorld">Hello World</div>
<div ng-controller="Sub1Ctrl">
<button type="button" ng-click="helloWorld = false">Hide Hello World</button>
</div>
<div ng-controller="Sub2Ctrl">
<button type="button" ng-click="helloWorld = false">Hide Hello World</button>
</div>
</div>
Your Main Controller
angular.module('MyModule').controller('MainCtrl', function( $scope ){
$scope.helloWorld = true;
});
All is fine and dandy and your hello world element shows as expected. Now try clicking one of those buttons which sets helloWorld to false. Your Sub1Ctrl will now look like this:
angular.module('MyModule').controller('Sub1Ctrl', function( $scope ){
$scope.helloWorld = false;
});
But your MainCtrl will still be
angular.module('MyModule').controller('MainCtrl', function( $scope ){
$scope.helloWorld = true;
});
Why is that? Well because when Angular evaluates "helloWorld = false" it sets $scope["helloWorld"] = false internally. The issue is because your setting it on the lower Sub1Ctrl, the higher controller is never set.
If you change your Html and MainCtrl to this:
<div ng-controller="MainCtrl">
<div ng-show="myModel.helloWorld">Hello World</div>
<div ng-controller="Sub1Ctrl">
<button type="button" ng-click="myModel.helloWorld = false">Hide Hello World</button>
</div>
<div ng-controller="Sub2Ctrl">
<button type="button" ng-click="myModel.helloWorld = false">Hide Hello World</button>
</div>
</div>
angular.module('MyModule').controller('MainCtrl', function( $scope ){
$scope.myModel = {
helloWorld: true
}
});
Then Angular will look up and see if the object exists in the scope's prototype (which is how controllers inherit through the Scope Hierarchy) before setting the value.
So now Angular evaluates "myModel.helloWorld" on the Sub1Ctrl which ends up finding "myModel" on the parent MainCtrl and so properly sets the helloWorld property to false.
Because then you keep the data in one place. A child scope (which is not an isolated scope) can shadow a property "message" for example. So the parent scope and the child scope have a "message" property. But if the parent scope has "data.message" property, the child scope cannot shadow this, unless first a property "data" is created in the child scope, and then "data.message", but this is not what AngularJS does. But AngularJS indeed creates first "data" and then "data.message" in the parent scope.
In an attempt to clean up my partials I recently moved some of my global menus into seperate templates which I now include in the views which need them. As the menu (including a search bar) is global I have created a service which keeps track of the state of the menu and so on.
Problem is something funny happened after I started including.
HTML (Jade) of the View
div(ng-controller='HeroUnitCtrl', ng-include src='heroTemplate')
div(ng-controller='MainSearchBarCtrl', ng-include src='searchBarTemplate')
div.row-fluid
div.span12
table.table.table-striped.table-bordered
tr
th
a(ng-click='setOrder("id")') ID#
th
a(ng-click='setOrder("client.name")') Kunde
th
a(ng-click='setOrder("technician.name")') Tekniker
th
a(ng-click='setOrder("createdAt")') Opprettet
th
a(ng-click='setOrder("statusString")') Status
tr(ng-repeat='record in records | orderBy:orderProp | filter:searchBar')
td
a(ng-href='#/records/show/{{record.id}}') {{record.id}}
td {{record.client.name}}
td {{record.technician.name}}
td {{record.createdAt}}
td {{record.statusString}}
HTML (Jade) searchBarTemplate
input#searchField.input-xxlarge(type='text', placeholder='placeholder', ng-change='searchBarUpdated()', ng-model='searchBar')
Now to the bit I really don't understand,
MainSearchBarCtrl
function MainSearchBarCtrl(MainSearchBarService, $scope, $location) {
$scope.searchBarTemplate = 'partials/main-searchbar';
$scope.searchBar = 'Hello World!';
$scope.searchBarUpdated = function() {
console.log('Search bar update: ' + $scope.searchBar);
MainSearchBarService.searchBarUpdated($scope.searchBar);
}
}
Initially the value of searchBar is as expected "Hello World". However, if I append any text it still only prints "Hello World". Or, if I replace the text it prints undefined. So it seems the binding is broken, but I don't really see why this is happening. Worth mentioning is that this wasn't case before I moved my search bar into a separate template.
Any help is greatly appreciated.
As discussed in the comments above, ng-include creates a new child scope. So in your searchBarTemplate, using ng-model="searchBar" results in a new searchBar property being created on the child scope, which hides/shadows the parent searchBar property of the same name.
In the controller, define an object:
$scope.obj = {searchBar: 'Hello World!};
And then use
ng-model="obj.searchBar"
in your template. When objects are used (instead of primitives), the child scope does not create a new property. Rather, due to the way JavaScript prototypal inheritance works, the child scope will find the property on the parent scope.
See also https://stackoverflow.com/a/14146317/215945 which has a picture showing the child and parent scopes and how the child property hides/shadows the parent property if a primitive is used.
Note that using $parent is another option, but it is not "best practice".
Try using $parent instead of $scope in your included template
I have a directive with an isolate-scope (so that I can reuse the directive in other places), and when I use this directive with an ng-repeat, it fails to work.
I have read all the documentation and Stack Overflow answers on this topic and understand the issues. I believe I have avoided all the usual gotchas.
So I understand that my code fails because of the scope created by the ng-repeat directive. My own directive creates an isolate-scope and does a two-way data-binding to an object in the parent scope. My directive will assign a new object-value to this bound variable and this works perfectly when my directive is used without ng-repeat (the parent variable is updated correctly). However, with ng-repeat, the assignment creates a new variable in the ng-repeat scope and the parent variable does not see the change. All this is as expected based on what I have read.
I have also read that when there are multiple directives on a given element, only one scope is created. And that a priority can be set in each directive to define the order in which the directives are applied; the directives are sorted by priority and then their compile functions are called (search for the word priority at http://docs.angularjs.org/guide/directive).
So I was hoping I could use priority to make sure that my directive runs first and ends up creating an isolate-scope, and when ng-repeat runs, it re-uses the isolate-scope instead of creating a scope that prototypically inherits from the parent scope. The ng-repeat documentation states that that directive runs at priority level 1000. It is not clear whether 1 is a higher priority level or a lower priority level. When I used priority level 1 in my directive, it did not make a difference, so I tried 2000. But that makes things worse: my two-way bindings become undefined and my directive does not display anything.
I have created a fiddle to show my issue. I have commented out the priority setting in my directive. I have a list of name objects and a directive called name-row that shows the first and last name fields in the name object. When a displayed name is clicked, I want it to set a selected variable in the main scope. The array of names, the selected variable are passed to the name-row directive using two-way data-binding.
I know how to get this to work by calling functions in the main scope. I also know that if selected is inside another object, and I bind to the outer object, things would work. But I am not interested in those solutions at the moment.
Instead, the questions I have are:
How do I prevent ng-repeat from creating a scope that prototypically inherits from the parent scope, and instead have it use my directive's isolate-scope?
Why is priority level 2000 in my directive not working?
Using Batarang, is it possible to know what type of scope is in use?
Okay, through a lot of the comments above, I have discovered the confusion. First, a couple of points of clarification:
ngRepeat does not affect your chosen isolate scope
the parameters passed into ngRepeat for use on your directive's attributes do use a prototypically-inherited scope
the reason your directive doesn't work has nothing to do with the isolate scope
Here's an example of the same code but with the directive removed:
<li ng-repeat="name in names"
ng-class="{ active: $index == selected }"
ng-click="selected = $index">
{{$index}}: {{name.first}} {{name.last}}
</li>
Here is a JSFiddle demonstrating that it won't work. You get the exact same results as in your directive.
Why doesn't it work? Because scopes in AngularJS use prototypical inheritance. The value selected on your parent scope is a primitive. In JavaScript, this means that it will be overwritten when a child sets the same value. There is a golden rule in AngularJS scopes: model values should always have a . in them. That is, they should never be primitives. See this SO answer for more information.
Here is a picture of what the scopes initially look like.
After clicking the first item, the scopes now look like this:
Notice that a new selected property was created on the ngRepeat scope. The controller scope 003 was not altered.
You can probably guess what happens when we click on the second item:
So your issue is actually not caused by ngRepeat at all - it's caused by breaking a golden rule in AngularJS. The way to fix it is to simply use an object property:
$scope.state = { selected: undefined };
<li ng-repeat="name in names"
ng-class="{ active: $index == state.selected }"
ng-click="state.selected = $index">
{{$index}}: {{name.first}} {{name.last}}
</li>
Here is a second JSFiddle showing this works too.
Here is what the scopes look like initially:
After clicking the first item:
Here, the controller scope is being affected, as desired.
Also, to prove that this will still work with your directive with an isolate scope (because, again, this has nothing to do with your problem), here is a JSFiddle for that too, the view must reflect the object. You'll note that the only necessary change was to use an object instead of a primitive.
Scopes initially:
Scopes after clicking on the first item:
To conclude: once again, your issue isn't with the isolate scope and it isn't with how ngRepeat works. Your problem is that you're breaking a rule that is known to lead to this very problem. Models in AngularJS should always have a ..
Without directly trying to avoid answering your questions, instead take a look at the following fiddle:
http://jsfiddle.net/dVPLM/
Key point is that instead of trying to fight and change the conventional behaviour of Angular, you could structure your directive to work with ng-repeat as opposed to trying to override it.
In your template:
<name-row
in-names-list="names"
io-selected="selected">
</name-row>
In your directive:
template:
' <ul>' +
' <li ng-repeat="name in inNamesList" ng-class="activeClass($index)" >' +
' <a ng-click="setSelected($index)">' +
' {{$index}} - {{name.first}} {{name.last}}' +
' </a>' +
' </li>' +
' </ul>'
In response to your questions:
ng-repeat will create a scope, you really shouldn't be trying to change this.
Priority in directives isn't just execution order - see: AngularJS : How does the HTML compiler arrange the order for compiling?
In Batarang, if you check the performance tab, you can see the expressions bound for each scope, and check if this matches your expectations.
I think I'm missing something simple (and important) here. I'm using an included template that contains an input that's mapped to some value:
<div ng-controller="Ctrl">
<div ng-include src="'template.html'"></div>
</div>
<!-- template -->
<script type="text/ng-template" id="template.html">
<input ng-model="testvalue" />
</script>
Controller:
function Ctrl($scope) {
$scope.testvalue= "initial value";
}
Alerting the value of $scope.testvalue always shows the initial value, not the updated value (when you type in the input). Help me Obi-Wan. You're our only hope.
Fiddle: http://jsfiddle.net/h5aac/
This is the all too common of binding to a primitive instead of an object. The value of the string gets passed around and not a reference to an object. If you use an object instead of a primitive, it works fine. Something like this in your scope.
$scope.foo = {testvalue: "initial value"};
See http://jsfiddle.net/h5aac/2/
Also:
Using `ng-model` within a transcluded directive in AngularJS
binding issue when a directive in a ngRepeat
AngularJS - updating scope value with asynchronous response
I'm sure there are more...
An alternative to referencing an object property in the parent scope is to use $parent to access the primitive in the parent scope:
<input ng-model="$parent.testvalue" />
ng-include creates a child scope. That scope prototypically inherits from Ctrl's parent scope. Here's how the 3 variations work:
$parent.testvalue ties the model to the property in the parent scope
testvalue by itself ties the model to a new property that will be created on the child scope. This property "shadows/hides" the parent scope property by the same name.
foo.testvalue (e.g., see #dnc253's answer) also ties the model to a parent property. It works like this: Javascript doesn't see/find 'foo' in the child scope, so it looks for it in the parent scope (due to prototypical inheritance) and finds it there.
To see what the child scope looks like, use your original fiddle, and add this code to your template somewhere:
<a ng-click="showScope($event)">show scope</a>
And add this code to your Ctrl:
$scope.showScope = function(e) {
console.log(angular.element(e.srcElement).scope());
}
Before you type into the textbox, click the "show scope" link. In the console (I'm using Chrome), you can expand the "Child" scope and see it does not contain a testvalue property yet (which I find surprising, because I don't know how it is displaying the "initial value" in the textbox). You can expand the $parent and you'll see the testvalue property there -- a property with this name appears to only be in the parent scope at this point.
Now, clear the console, type into the textbox, and click the "show scope" link again. You'll see that the "Child" scope now has a new testvalue property. It shadows/hides the parent property. So, things in the child scope see the child scope testvalue property, and things in the parent scope see the parent scope testvalue property.
Update: FYI, I recently wrote an extensive answer/article about scope prototypical inheritance that explains the above concepts in much more detail, with lots of pictures: What are the nuances of scope prototypal / prototypical inheritance in AngularJS?