I am trying to bind a checkbox to scope using ng-model. The checkbox's initial state corresponds to the scope model just fine, but when I check/uncheck the checkbox, the model does not change. Some things to note is that the template is dynamically loaded at runtime using ng-include
app.controller "OrdersController", ($scope, $http, $location, $state, $stateParams, Order) ->
$scope.billing_is_shipping = false
$scope.bind_billing_to_shipping = ->
console.log $scope.billing_is_shipping
<input type="checkbox" ng-model="billing_is_shipping"/>
When I check the box the console logs false, when I uncheck the box, the console again logs false. I also have an order model on the scope, and if I change the checkbox's model to be order.billing_is_shipping, it works fine
I struggled with this problem for a while. What worked was to bind the input to an object instead of a primitive.
<!-- Partial -->
<input type="checkbox" ng-model="someObject.someProperty"> Check Me!
// Controller
$scope.someObject.someProperty = false
If the template is loaded using ng-include, you need to use $parent to access the model defined in the parent scope since ng-include if you want to update by clicking on the checkbox.
<div ng-app ng-controller="Ctrl">
<div ng-include src="'template.html'"></div>
</div>
<script type="text/ng-template" id="template.html">
<input type="checkbox" ng-model="$parent.billing_is_shipping" ng-change="checked()"/>
</script>
function Ctrl($scope) {
$scope.billing_is_shipping = true;
$scope.checked = function(){
console.log($scope.billing_is_shipping);
}
}
DEMO
In my directive (in the link function) I had created scope variable success like this:
link: function(scope, element, attrs) {
"use strict";
scope.success = false;
And in the scope template included input tag like:
<input type="checkbox" ng-model="success">
This did not work.
In the end I changed my scope variable to look like this:
link: function(scope, element, attrs) {
"use strict";
scope.outcome = {
success : false
};
And my input tag to look like this:
<input type="checkbox" ng-model="outcome.success">
It now works as expected. I knew an explanation for this, but forgot, maybe someone will fill it in for me. :)
Expanding on Matt's answer, please see this Egghead.io video that addresses this very issue and provides an explanation for: Why binding properties directly to $scope can cause issues
see: https://groups.google.com/forum/#!topic/angular/7Nd_me5YrHU
Usually this is due to another directive in-between your ng-controller
and your input that is creating a new scope. When the select writes
out it value, it will write it up to the most recent scope, so it
would write it to this scope rather than the parent that is further
away.
The best practice is to never bind directly to a variable on the scope
in an ng-model, this is also known as always including a "dot" in
your ngmodel.
Related
I have a nested directive with an isolated scope. An Array of objects is bound to it via 2 way data binding.
.directive('mapMarkerInput',['mapmarkerService', '$filter', '$timeout',function(mapMarkerService, $filter, $timeout) {
return {
restrict: 'EA',
templateUrl:'templates/mapmarkerInputView.html',
replace: true,
scope: {
mapmarkers: '='
},
link: function($scope, element, attrs) {
//some other code
$scope.addMapmarker = function($event) {
var mapmarker = {};
var offsetLeft = $($event.currentTarget).offset().left,
offsetTop = $($event.currentTarget).offset().top;
mapmarker.y_coord = $event.pageY - offsetTop;
mapmarker.x_coord = $event.pageX - offsetLeft;
mapmarker.map = $scope.currentMap;
$scope.mapmarkers = $scope.mapmarkers.concat(mapmarker);
};
$scope.deleteMapmarker = function(mapmarker) {
var index = $scope.mapmarkers.indexOf(mapmarker);
if(index !== -1) {
$scope.mapmarkers.splice(index,1);
}
};
//some other code
)
}]);
These 2 functions are triggered via ng-click:
<img ng-if="currentMap" ng-click="addMapmarker($event)" ng-src="/xenobladex/attachment/{{currentMap.attachment.id}}" />
<div class="mapmarker-wrapper" ng-repeat="mapmarker in shownMapmarkers" ng-click="setZIndex($event)" style="position: absolute; top: {{mapmarker.y_coord}}px; left: {{mapmarker.x_coord}}px;">
<!-- some other code -->
<div class="form-group">
<label>Name:</label>
<input ng-model="mapmarker.name" value="mapmarker.name" class="form-control" type="text">
</div>
<div class="form-group">
<label>Description:</label>
<input ng-model="mapmarker.description" value="mapmarker.description" class="form-control" type="text">
</div>
<button class="btn btn-danger" ng-click="deleteMapmarker(mapmarker)">Delete</button>
</div>
As you can see I am binding the name and description directly via ng-model and that works just fine. The properties are also available in the parent scope, but neither the delete nor the add works (its changed within the directives scope, but not the parent scope).
As far as I understand these changes should be applied, because I'm calling these functions via ng-click and I have other examples where this works. The only difference is, that I am binding to an array of objects and not a single object / property.
I tried using $timer and updateParent() ($scope.$apply() does not work -> throws an exception that the function is already within the digest cycle) but with no success, so it looks like these changes are not watched at all.
The directive code looks like this:
<map-marker-input ng-if="$parent.formFieldBind" mapmarkers="$parent.formFieldBind"></map-marker-input>
It is nested within a custom form field directive which gets the correct form field template dynamically and has therefore template: '<div ng-include="getTemplate()"></div>' as template, which creates a new child scope - that's why the $parent is needed here.
The binding definitely works in one way, the expected data is available within the directive and if I'm logging the data after changing it via delete or add, it's also correct, but only from the inside of the directive.
Because ng-model works I guess there might be a simple solution to the problem.
UPDATE
I created a plunkr with a simplified version:
http://plnkr.co/85oNM3ECFgCzyrSPahIr
Just click anywhere inside the blue area and new points are added from within the mapmarker directive. Right now I dont really prevent adding points if you delete or edit these - so you'll end up with a lot of points fast ;-)
There is a button to show the data from the parent scope and from the child scope.
If you edit the name or description of the one existing point that will also be changed in the parent scope (bound via ng-model). But all new points or deletions are ignored (bound within the functions called via ng-click).
If you want to update the parent scope, you need to access it via $parent once more,
i change
mapmarkers="$parent.formFieldBind"
to :
mapmarkers="$parent.$parent.formFieldBind"
ng-include create one more scope, so you need to access the parent once more.
http://plnkr.co/edit/27qF6ABUxIum8A3Hrvmt?p=preview
I am using bootstrap-ui more specifically modal windows. And I have a form in a modal, what I want is to instantiate form validation object. So basically I am doing this:
<form name="form">
<div class="form-group">
<label for="answer_rows">Answer rows:</label>
<textarea name="answer_rows" ng-model="question.answer_rows"></textarea>
</div>
</form>
<pre>
{{form | json}}
</pre
I can see form object in the html file without no problem, however if I want to access the form validation object from controller. It just outputs me empty object. Here is controller example:
.controller('EditQuestionCtrl', function ($scope, $modalInstance) {
$scope.question = {};
$scope.form = {};
$scope.update = function () {
console.log($scope.form); //empty object
console.log($scope.question); // can see form input
};
});
What might be the reasons that I can't access $scope.form from controller ?
Just for those who are not using $scope, but rather this, in their controller, you'll have to add the controller alias preceding the name of the form. For example:
<div ng-controller="ClientsController as clients">
<form name="clients.something">
</form>
</div>
and then on the controller:
app.controller('ClientsController', function() {
// setting $setPristine()
this.something.$setPristine();
};
Hope it also contributes to the overall set of answers.
The normal way if ng-controller is a parent of the form element:
please remove this line:
$scope.form = {};
If angular sets the form to your controllers $scope you overwrite it with an empty object.
As the OP stated that is not the case here. He is using $modal.open, so the controller is not the parent of the form. I don't know a nice solution. But this problem can be hacked:
<form name="form" ng-init="setFormScope(this)">
...
and in your controller:
$scope.setFormScope= function(scope){
this.formScope = scope;
}
and later in your update function:
$scope.update = function () {
console.log(this.formScope.form);
};
Look at the source code of the 'modal' of angular ui bootstrap, you will see the directive has
transclude: true
This means the modal window will create a new child scope whose parent here is the controller $scope, as the sibling of the directive scope. Then the 'form' can only be access by the newly created child scope.
One solution is define a var in the controller scope like
$scope.forms = {};
Then for the form name, we use something like forms.formName1. This way we could still access it from our controller by just call $scope.forms.formName1.
This works because the inheritance mechanism in JS is prototype chain. When child scope tries to create the forms.formName1, it first tries to find the forms object in its own scope which definitely does not have it since it is created on the fly. Then it will try to find it from the parent(up to the prototype chain) and here since we have it defined in the controller scope, it uses this 'forms' object we created to define the variable formName1. As a result we could still use it in our controller to do our stuff like:
if($scope.forms.formName1.$valid){
//if form is valid
}
More about transclusion, look at the below Misco's video from 45 min. (this is probably the most accurate explanation of what transcluded scopes are that I've ever found !!!)
www.youtube.com/watch?v=WqmeI5fZcho
No need for the ng-init trickery, because the issue is that $scope.form is not set when the controller code is run. Remove the form = {} initialization and get access to the form using a watch:
$scope.$watch('form', function(form) {
...
});
I use the documented approach.
https://docs.angularjs.org/guide/forms
so, user the form name, on "save" click for example just pass the formName as a parameter and hey presto form available in save method (where formScopeObject is greated based upon the ng-models specifications you set in your form OR if you are editing this would be the object storing the item being edited i.e. a user account)
<form name="formExample" novalidate>
<!-- some form stuff here -->
Name
<input type="text" name="aField" ng-model="aField" required="" />
<br /><br />
<input type="button" ng-click="Save(formExample,formScopeObject)" />
</form>
To expand on the answer by user1338062: A solution I have used multiple times to initialize something in my controller but had to wait until it was actually available to use:
var myVarWatch = $scope.$watch("myVar", function(){
if(myVar){
//do whatever init you need to
myVarWatch(); //make sure you call this to remove the watch
}
});
For those using Angular 1.5, my solution was $watching the form on the $postlink stage:
$postLink() {
this.$scope.$watch(() => this.$scope.form.$valid, () => {
});
}
I have a directive set up here http://jsfiddle.net/screenm0nkey/8Cw4z/3 which has two bindings to the same scope property, but for some reason the binding in the directive's template property doesn't update when the model changes (after typing in the input).
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count">
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
}
};
});
But if I move the input position in the markup it works and both bindings update.
<input type="text" ng-model="count">
<test>
<h3>Inner {{count}}</h3>
</test>
http://jsfiddle.net/screenm0nkey/dCvZk/3
Can anyone explain why the position of the input containing the binding, would have an affect the bindings. I assumed that during the digest loop the watchers for both binding would be updated regardless of the position of the markup.
Many thanks
To me, this seems purely to be a scope issue. Lets take a look at the markup that is generated by both:
Not working:
<body ng-app="App" class="ng-scope">
<h1 class="ng-binding">Outer1 <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner 1</h3>
<input type="text" ng-model="count" class="ng-scope ng-pristine ng-valid">
</div>
</h1>
</body>
Working:
<body ng-app="App" class="ng-scope">
<input type="text" ng-model="count" class="ng-valid ng-dirty">
<h1 class="ng-binding">Outer <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner </h3>
</div>
</h1>
</body>
The ng-scope class is a useful marker for where Angular is declaring a new scope.
You can see by the markup that the in the working example both the count properties are enclosed in the scope that is attached to body. So, in this case, the directive scope is a child of the body scope (and therefore has access to it).
However, In the example that is not working, the Outer1 property is sitting outside of the scope that the input is in.
The Angular Scope documentation covers this well. The scopes are arranged in a hierarchy with child scopes having access to parent scopes (but not the other way around):
The application can have multiple scopes, because some directives
create new child scopes (refer to directive documentation to see which
directives create new scopes). When new scopes are created, they are
added as children of their parent scope. This creates a tree structure
which parallels the DOM where they're attached
Long story short - as others have said, this is a scope issue. Using the "ng-transclude" directive creates a new scope. When a new scope is created values from the old scope will be accessible in the new scope (hence the first replace) but after that only objects that are shared between the old/new scope will be updated. That is why using an object would work, but using a value will not.
In your case placing the input field inside of the ng-transclude causes this to only edit the value in that scope, not the value in the parent scope (which is where the count for the "test" directive is pulled from).
Incidentally, this can be an issue with repeaters (ng-repeat) as well as other directives. Its best to use a tool such as "Batarang" in order to find issues such as this. It allows you to look at what is in each scope and determine why the screen isn't showing the "correct" data. Hope that helps explain further!
Add ng-change to input , it should work. The problem is that controller into directive doesn't know about count change.
JS
var App = angular.module('App', []);
App.directive('test', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer {{this.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
$scope.onChange = function(count){
$scope.count = count;
}
}
};
});
HTML
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count" ng-change="onChange(count)">
</test>
Demo Fiddle
The order matters because of the difference between creating a property on the scope versus actually using an object bound to the scope (especially when a transclude creates a new child scopr). Best practice is to use an object on the scope and bind properties to that object when scope issues can come into play with directives and transcludes.
If you change your code to this, it will work as you were expecting and order does not matter. Notice that I am creating a scope object and placing the count as a property on that object.
<test>
<h3>Inner {{data.count}}</h3>
<input type="text" ng-model="data.count"/>
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{data.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.data = {};
$scope.data.count = 1;
}
};
});
This is a great tutorial on this subject. Props to EggHead. https://egghead.io/lessons/angularjs-the-dot
It's a scoping issue.
$scope.count = 1; adds the property count to the scope that <test> is in. Let's call it parent scope.
ng-transclude creates a new scope, let's call it child scope. When <h3>Inner {{count}}</h3> is evaluated, the child scope has no property count so it's read from the parent scope.
<input type="text" ng-model="count"> binds the value of the input to the property count in the child scope. As soon as you enter something the property will be created if it's not there yet. From this point on <h3>Inner {{count}}</h3> gets its value from the child scope.
Scopes in angular are simple JavaScript objects and are connected to their parents via prototypes. So before you enter something the child scope looks something like
{
prototype: { // = parent scope
count: 1
}
}
When you change the value to, say, 5, the scope looks something like
{
count: 5,
prototype: { // = parent scope
count: 1
}
}
Because data binding does something like scope.count = 5.
Here's a work around
Change $scope.count to
$scope.helper = {
count: 1
}
and refactor the rest.
Check this video out for an explanation.
It seems that we cannot override this since ngTransclude will use $transclude function directly.
See: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngTransclude.js
and: http://docs.angularjs.org/api/ng.$compile
transcludeFn - A transclude linking function pre-bound to the correct transclusion scope. The scope can be overridden by an optional first argument. This is the same as the $transclude parameter of directive controllers. function([scope], cloneLinkingFn).
I wanted to make a directive that would essentially act like a specialized input field. After some logic & user input, the 'value' attribute would be populated with a string of comma separated timeslots (hh:mm).
<time-slot value=""></time-slot>
becomes
<time-slot value="01:00,02:00,03:00"></time-slot>
I'd like to provide the flexibility for anyone to place a scope reference in the 'value' attribute tag -- whenever the attribute value is updated, so is the scope reference. (In the code below, myModel.times would be in the MyController scope).
<div ng-controller="MyController">
<time-slot value="{{ myModel.times }}"></time-slot>
</div>
I have had no problems accessing the 'value' attribute in the directive. However, I have not achieved two-way binding -- myModel.times never captures the changed value, even though the contents of the attribute have been changed when I inspect the element during runtime. I am using $attrs.$set to alter the value attribute.
I'm not sure if I'm missing something conceptually, or just missing some extra syntax. To keep this directive modular and shareable, I don't want to use a service to share data between the controller and directive, nor do I want to use a cascading scope. I think it would be optimal if the value attribute can simply be referenced by a scope variable and used as desired, much like a simple input tag:
<input ng-model="model.someText"></input>
An example with two-way data binding: See plunkr.
angular.module('myApp', [])
.directive('timeSlots', function() {
return {
scope: { value: '=' },
link: function($scope, $elem, $attrs) {
// you can access $scope.value here (after it has been interpolated)
}
};
})
.controller('MainCtrl', ['$scope', function($scope) {
$scope.value = 42;
}]);
In HTML:
<div ng-controller="MainCtrl">
<time-slots value="value"></time-slots>
</div>
EDIT: see jsfiddle
I have a list of items, that I show as text via a directive (all is simplified here, it's more comlicated than just text, but it's the same in principle). Like so:
<body ng:controller="BaseController">
...
<div ng:controller="Controller">
<itemdir ng:repeat="item in items" item="item"></item>
</div>
...
<input ng:model="currentItem" />
</body>
When I click it, it should show the content of the clicked item in an input.
items array as well as currentItem belong to BaseController scope.
The directive produces a template (see below) with ng:click which should change BaseController scope's property (called currentItem). However it does not do anything to it (input value is not changed to the new current item). In Batarang for Chrome I can see that the currentItem property is visible and changed in the scope of the directive but not of the BaseController.
module.directive 'itemdir', () ->
restrict: 'E'
replace: true
template: '<div ng:click="show(item)"></div>'
controller: 'EditorController'
scope:
item: '=item'
link: ($scope, $element, $attrs) ->
update = ->
$element.html($scope.item)
$scope.$watch('item', update)
For changing the property I tried a method show(item) which is defined in the BaseController's scope which only assigns the item parameter to $scope.currentItem.
It doesn't work even when I change the ng:click value from show(item) to currentItem = item
I know this is some scope issue, but it seems I still don't grasp all the details of it.
So, looking at the provided jsFiddle we can see that the BaseController is being used both in a directive and in top div. This introduced a subtle issue since it was possible to invoke the show(item) method from top-buttons and HTML produced by directives, but those methods were invoked on different controllers and writing to different scopes.
Now, it is hard to deduce from your question if the use of BaseController in a directive was intentional or not (in the question the directive has the EditorController) but assuming that this was by accident and you want to keep BaseController for a div and still invoke methods on it from a directive you need to take special care when creating isolated scopes (as the name implies those are really isolated so not inheriting from a parent scope). Basically you need to make sure that the show method is available in an isolated scope and points to the right method in the parent scope.
Taking your example you would define your directive like this (please note show : '&ngClick'):
module.directive('itemdir', function () {
return {
restrict:'E',
replace:true,
template:'<div ng:click="show(item)" class="clickable"></div>',
scope : {item : '=', show : '&ngClick'},
link:function ($scope, $element, $attrs) {
$element.html($scope.item)
}
}
});
Here is the working jsFiddle: http://jsfiddle.net/pkozlowski_opensource/M9B93/
In the future you might find AngularJS Batarang extension for Chrome (http://blog.angularjs.org/2012/07/introducing-angularjs-batarang.html) useful as it allows to visualize scopes and their content.