I'm new to controllerAs syntax of angular and just trying to understand how it works with directive. I've created one directive for password validation. I want to make some flag true based on conditions and those will be used in parent template for displaying error messages. I'm not getting how can I achieve this!
JSFiddle
VIEW
<div ng-app="myapp">
<fieldset ng-controller="PersonCtrl as person">
<input name="emailID" type="text" ng-model="person.first" >
<input name="pass" type="password" ng-model="person.pass" password-validator>
<p ng-show="person.showMsg">Password validation message here.</p>
</fieldset>
</div>
Directive
myapp.directive('passwordValidator',function() {
return {
controller : PasswordCtrl,
controllerAs : 'dvm',
bindToController : true,
require : ['ngModel','passwordValidator'],
link : function(scope,ele,attrs,ctrls) {
var person = ctrls[1];
var ngModelCtrl = ctrls[0];
scope.$watch(function() {
return ngModelCtrl.$modelValue;
},function(newVal) {
if(newVal!='') {
person.showMsg = true;
} else {
person.showMsg = false;
}
console.log(person.showMsg);
});
}
}
function PasswordCtrl() {
}
});
Specially I want to understand why and how below watch is working fine!
// Why this below is also working, can anyone explain what's going behind!!
scope.$watch('person.pass',function(newVal) {
console.log("Watch fires");
});
This is just for learning purpose so please explain how controllerAs and bindToController works!
I know this was not part of your question, I will get to it, but using directive 'ng-controller' is an anti-pattern. If if are interested why I can explain in a separate post but in short it makes code much harder to follow.
Now, to get to the heart of your question.
From reading the Angular documentation for bindToController it would appear that if you are not also creating an isolated scope, i.e. scope: true or scope: {} it does not do anything.
Personally I have never used it before and does not seem particularly useful.
Using ng-controller is in essence adding a property to the current scope with that controller object.
So:
<fieldset ng-controller="PersonCtrl as person">
Is effectively saying, (in a contrived way):
$scope.person = new PersonCtrl();
Your directive passwordValidator which is using the controllerAs syntax within it is basically doing:
$scope.dvm= new PasswordCtrl();
In this case you effectively have a scope object that looks like:
$scope = {
person = new PersonCtrl(),
dvm: new PasswordCtrl()
}
Your person controller and dvm controller are sibling objects.
Within your passwordValidator directive you are requiring in its controller, which is the dvm object. Using that dvm object are you setting person.showMsg which is the same as doing:
$scope.dvm.person.showMsg = <value>
The dvm object does not have a way to access the person object, as they are siblings, on the $scope. So you need to use the $scope itself to access the person object. You would need to do:
$scope.person.showMsg = <value>
Although this assumes that person exists on the scope, which is a dangerous assumption.
your example is kinda messy but ill try to answer your questions.
// Why this below is also working, can anyone explain what's going behind!!
scope.$watch('person.pass',function(newVal) {
console.log("Watch fires");
});
this works because your directive is using SAME scope and variables on it as its parent controller.
this.checkDirCtrl = function() {
console.log($scope.dvm);
}
this is undefined because you are using controllerAs on SAME scope so new variable called dvm is not created and all your dvm controller variables are initialized on parent scope.
That means that you dont need to 'inject' person controller into directive because you already have controller instance on directive scope. So just set your variable 'showMsg' like this and it will work like a magic!
if(newVal!='') {
scope.person.showMsg = true;
} else {
scope.person.showMsg = false;
}
console.log(scope.person.showMsg);
I made a fiddle for you: JSFiddle
Related
I have a curious case that I can't figure out...
I have a directive on my app like so:
app.directive('cartTotal', function() {
return {
template: "<i ui-sref='cart' class='fa fa-shopping-basket'></i><span class='items'>#{{cartQTotal.total}}</span>"
};
});
When I load the page, this function fires:
if(localStorage.getItem("cart") != null)
{
console.log("makeacart");
var cart = JSON.parse(localStorage.getItem("cart"));
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
$('.fixed-cart').animateCss('bounce');
}
This works.
But if I modify $scope.cartQTotal outside of this, such as in a function (still in the parent controller but derived from an ng-click()) for example:
$scope.add2Cart = function(name){
var cart = JSON.parse(localStorage.getItem("cart"));
for(var zz = 0;zz<cart.length;zz++)
{
if(cart[zz].item == name)
{
console.log("already in cart");
$('.fixed-cart').animateCss('bounce');
return;
}
}
cart.push({item:name,cartQ:1});
localStorage.setItem("cart", JSON.stringify(cart));
console.log("makeacartii");
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
console.log($scope.cartQTotal.total);//THE NUMBER I WANT
$('.fixed-cart').animateCss('bounce');
}
On //The Number I Want line I get the proper number, as in the variable is correct but my directive template doesn't update. I don't understand why not.
Please assist.
Edit (from the docs):
Observing directives, such as double-curly expressions {{expression}},
register listeners using the $watch() method. This type of directive
needs to be notified whenever the expression changes so that it can
update the view.
So I guess the question is how do I notify the directive properly?
EDIT 2:
Looking at it using the nginspector extension, it appears I have two scopes with cartQTotal rather than one, this remains constant whether or not I have the directive.
I am very confused because I have my controller scope and then a duplicate scope with all the same variables but the cartQTotal changes in one scope and not the other. Why would I have a duplicate but unnamed controller scope?
This is because your directive and $scope and the controller where data is updating both are different..
So you need to pass your controller data to your directive so that it will get modified. For this purpose you can use $broadcast (but make sure you know about it because in large application its not good practice to use it).
So Try this
Controller
cart.push({item:name,cartQ:1});
localStorage.setItem("cart", JSON.stringify(cart));
console.log("makeacartii");
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
console.log($scope.cartQTotal.total);//THE NUMBER I WANT
$('.fixed-cart').animateCss('bounce');
$rootScope.$broadcast("cartUpdated",$scope.cartQTotal);
directive
$scope.$on('eventEmitedName', function(event, data) {
$scope.cartQTotal = data;
});
It was a problem as elucidated here: How do I share $scope data between states in angularjs ui-router?
Basically I didn't realize that my ui-router configuration was creating a seperate instance of my controller. Changing my code as specified in that answer allowed it to work properly, even though I wasn't changing states it still affected the directive's ability to communicate with the proper controller instance.
Data you wish to use within your directive that is manipulated outside of the directive should be passed in using bindings. There's a great short read here that shows you how. Personally, I use method 6 the most.
The gist is - you add to your directive's returned object:
scope: {
yourVariable: '=', //use =? for optional variables
}
And then use it in your directive as such:
<span>{{your-variable}}</span>
And bind to it as such:
<my-directive your-variable="myControllerVariable"></my-directive>
I have an existing model compiled from java to javascript using GWT 2.7. Each property in this model has a getValue() and a setValue() method. The method names are not mangled.
I want to use these properties in {{}} expressions, specifically for binding using ngModel. I'm using the "getterSetter" option and a function in the $scope that wraps getValue()/setValue() in an angularJS getterSetter function.
The problem is that I don't want to duplicate this function in each scope. Is there any way of accessing a global function in an angularJS expression?
From the docs:
Angular expressions do not have access to global variables like window, document or location. This restriction is intentional. It prevents accidental access to the global state – a common source of subtle bugs.
Can you just attach the function to the model prototype? That way you just can access it wherever you have an instance of the model.
Alternatively, you could create a wrapper class (basically a view-model) that has the interface you need, and delegates to the real model instance.
After quite some searching and experimenting I found that I can use the directive's controller to add the function to the scope.
.directive('wrapped', function() {
return {
controller: function($scope) {
$scope.wrapper = function(prop) {
// turns signal into getterSetter
return function(newVal) {
if (angular.isDefined(newVal)) {
prop.setValue(newVal);
}
return prop.getValue();
}
}
},
scope : {
model : '&'
},
templateUrl : 'template.html',
}
})
A function in the scope can then be used in expressions in the template.
<p class="field {{model().status().isValid() ? 'valid' : 'invalid'}}">
<span class="label">{{label}}:</span>
<input type="text" ng-model="wrap(model())" ng-model-options="{getterSetter: true}" ></input>
</p>
I would love to hear from more experienced AngularJS programmers if there is a more idiomatic way to write this.
I have a custom directive that I'm using in my templates. It does a bit of DOM work for me. I would like the host view/controller that I'm using the directive in to be able to run methods on my directive (and it's controller). But I'm not sure how best to call into the directives scope.
Example Fiddle
My view code:
<div ng-app="app">
<div ng-controller="MainCtrl">
<h3>Test App</h3>
<button ng-click="scopeClear()">Parent Clear</button>
<div my-directive string="myString"></div>
</div>
</div>
Here is the custom directive:
angular.module('components', []).directive('myDirective', function() {
function link(scope, element, attrs) {
scope.string = "";
scope.$watch(attrs.string, function(value) {
scope.string = value;
});
}
return {
controller: function($scope, $element) {
$scope.reset = function() {
$scope.string = "Hello";
}
$scope.clear = function() {
$scope.string = "";
}
},
template:
"<button ng-click='reset()'>Directive Reset</button>" +
"<button ng-click='clear()'>Directive Clear</button><br/>" +
"<input type='text' ng-model='string'>",
link: link
}
});
And controller:
angular.module('app', ['components']).controller('MainCtrl', function($scope) {
$scope.myString = "Hello";
$scope.scopeClear = function() {
// How do I get this to call the clear() method on myDirective
}
});
The workaround I found is jQuery('#my_directive').scope().myMethod(); But this seems wrong, like I'm missing some better part of angular to do this.
It also seems like and $emit isn't right here since I want a targeted method so it won't trigger on additional instances of the directive I have on the same page.
How would I access the directives methods from my parent controller?
I'm not sure I fully understand your objective here, and it's possible you could find a better pattern completely. Typically, directives display the state of the scope which is either an isolate scope (if they are self-sufficient) or a shared scope. Since you are not creating an isolate scope then they inherit the scope from the controller. If they are displaying data inherited from the controller then you don't want your controller calling into the directive, rather the directive will simply "redraw" itself whenever the properties in the controller change.
If you, instead, are looking to recalculate some stuff in your directives based on events from outside the directive you don't want any tight coupling - especially if building an entirely separate module. In that case, you might simply want to use $broadcast from the $scope within MainCtrl to broadcast an event that you may care about, and then your directive can provide the $on('eventName') handler. This way it's portable to any controller/scope that will fire such an event.
If you find yourself needing to know the exact properties in the controller or the exact functions within the directive then I would suggest that you have too-tightly coupled these pieces and they don't belong in separate modules since they could never be reused. Angular directives and controllers are not objects with functions, but objects that create scope and update frequently via $digest calls whenever properties in that scope change. So you may be able to find a way to better model the data, objects, and properties you are displaying. But I can't say without greater context.
I have a directive that takes in a collection and builds out a dropdown.
.directive("lookupdropdown", function () {
return {
restrict: 'E',
scope: {
collectionset: '=',
collectionchoice: '='
},
replace: true,
template: '<select class="input-large" ui-select2 ng-model="collectionchoice" data-placeholder="">' +
' <option ng-repeat="collection in repeatedCollection" value="{{collection.id}}">{{collection.description}}</option>' +
'</select>',
controller: ["$scope", function ($scope) {
$scope.repeatedCollection = new Array(); //declare our ng-repeat for the template
$scope.$watch('collectionset', function () {
if ($scope.collectionset.length > 0) {
angular.forEach($scope.collectionset, function (value, key) { //need to 'copy' these objects to our repeated collection array so we can template it out
$scope.repeatedCollection.push({ id: value[Object.keys(value)[0]], description: value[Object.keys(value)[1]] });
});
}
});
$scope.$watch('collectionchoice', function (newValue, oldValue) {
debugger;
$scope.collectionchoice;
});
} ]
}
});
This works fine. It builds out the drop down no problem. When I change the dropdown value, the second watch function gets called and I can see that it sets the value of collection choice to what I want. However, the collectionchoice that I have put into the directive doesn't bind to the new choice.
<lookupDropdown collectionset="SecurityLevels" collectionchoice="AddedSecurityLevel"></lookupDropdown>
That is the HTML markup.
This is the javascript:
$scope.SecurityLevels = new Array();
$scope.GetSecurityLevelData = function () {
genericResource.setupResource('/SecurityLevel/:action/:id', { action: "#action", id: "#id" });
genericResource.getResourecsList({ action: "GetAllSecurityLevels" }).then(function (data) {
$scope.AddedSecurityLevel = data[0].SCRTY_LVL_CD;
$scope.SecurityLevels = data;
//have to get security levels first, then we can manipulate the rest of the page
genericResource.setupResource('/UserRole/:action/:id', { action: "#action", id: "#id" });
$scope.GetUserRoles(1, "");
});
}
$scope.GetSecurityLevelData();
Then when I go to post my new user role, I set the user role field like this:
NewUserRole.SCRTY_LVL_CD = $scope.AddedSecurityLevel;
but this remains to be the first item EVEN though I have updated the dropdown, which according the watch function, it has changed to the correct value. What am I missing here?
You faced this issue because of the prototypical nature inheritance in Javascript. Let me try and explain. Everything is an object in Javascript and once you create an object, it inherits all the Object.Prototype(s), which eventually leads to the ultimate object i.e. Object. That is why we are able to .toString() every object in javascript (even functions) because they are all inherited from Object.
This particular issue on directives arises due to the misunderstanding of the $scope in Angular JS. $scope is not the model but it is a container of the models. See below for the correct and incorrect way of defining models on the $scope:
...
$scope.Username = "khan#gmail.com"; //Incorrect approach
$scope.Password = "thisisapassword";//Incorrect approach
...
$scope.Credentials = {
Username: "khan#gmail.com", //Correct approach
Password: "thisisapassword" //Correct approach
}
...
The two declarations make a lot of difference. When your directive updated its scope (isolated scope of directive), it actually over-rid the reference completely with the new value rather then updating the actual reference to the parent scope hence it disconnected the scope of the directive and the controller.
Your approach is as follows:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>
The problem with this approach is that although it works, but it not the recommended solution and here is why. What if your directive is placed inside another directive with another isolated scope between scope of your directive and the actual controller, in that case you would have to do $parent.$parent.AddedSecurityLevel and this could go on forever. Hence NOT a recommended solution.
Conclusion:
Always make sure there is a object which defines the model on the scope and whenever you make use of isolate scopes or use ng directives which make use of isolate scopes i.e. ng-model just see if there is a dot(.) somewhere, if it is missing, you are probably doing things wrong.
The issue here was that my directive was being transcluded into another directive. Making the scope im passing in a child of the directive it was in. So something like $parent -> $child -> $child. This of course was making changes to the third layer and second layer. But the first layer had no idea what was going on. This fixed it:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>
Probably silly question, but I have my html form with simple input and button:
<input type="text" ng-model="searchText" />
<button ng-click="check()">Check!</button>
{{ searchText }}
Then in the controller (template and controller are called from routeProvider):
$scope.check = function () {
console.log($scope.searchText);
}
Why do I see the view updated correctly but undefined in the console when clicking the button?
Thanks!
Update:
Seems like I have actually solved that issue (before had to come up with some workarounds) with:
Only had to change my property name from searchText to search.text, then define empty $scope.search = {}; object in the controller and voila... Have no idea why it's working though ;]
"If you use ng-model, you have to have a dot in there."
Make your model point to an object.property and you'll be good to go.
Controller
$scope.formData = {};
$scope.check = function () {
console.log($scope.formData.searchText.$modelValue); //works
}
Template
<input ng-model="formData.searchText"/>
<button ng-click="check()">Check!</button>
This happens when child scopes are in play - like child routes or ng-repeats.
The child-scope creates its own value and a name conflict is born as illustrated here:
See this video clip for more: https://www.youtube.com/watch?v=SBwoFkRjZvE&t=3m15s
Controller as version (recommended)
Here the template
<div ng-app="example" ng-controller="myController as $ctrl">
<input type="text" ng-model="$ctrl.searchText" />
<button ng-click="$ctrl.check()">Check!</button>
{{ $ctrl.searchText }}
</div>
The JS
angular.module('example', [])
.controller('myController', function() {
var vm = this;
vm.check = function () {
console.log(vm.searchText);
};
});
An example: http://codepen.io/Damax/pen/rjawoO
The best will be to use component with Angular 2.x or Angular 1.5 or upper
########
Old way (NOT recommended)
This is NOT recommended because a string is a primitive, highly recommended to use an object instead
Try this in your markup
<input type="text" ng-model="searchText" />
<button ng-click="check(searchText)">Check!</button>
{{ searchText }}
and this in your controller
$scope.check = function (searchText) {
console.log(searchText);
}
In Mastering Web Application Development with AngularJS book p.19, it is written that
Avoid direct bindings to scope's properties. Two-way data binding to
object's properties (exposed on a scope) is a preferred approach. As a
rule of thumb, you should have a dot in an expression provided to the
ng-model directive (for example, ng-model="thing.name").
Scopes are just JavaScript objects, and they mimic dom hierarchy. According to JavaScript Prototype Inheritance, scopes properties are separated through scopes. To avoid this, dot notation should use to bind ng-models.
Using this instead of $scope works.
function AppCtrl($scope){
$scope.searchText = "";
$scope.check = function () {
console.log("You typed '" + this.searchText + "'"); // used 'this' instead of $scope
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app>
<div ng-controller="AppCtrl">
<input ng-model="searchText"/>
<button ng-click="check()">Write console log</button>
</div>
</div>
Edit: At the time writing this answer, I had much more complicated situation than this. After the comments, I tried to reproduce it to understand why it works, but no luck. I think somehow (don't really know why) a new child scope is generated and this refers to that scope. But if $scope is used, it actually refers to the parent $scope because of javascript's lexical scope feature.
Would be great if someone having this problem tests this way and inform us.
I had the same problem and it was due to me not declaring the blank object first at the top of my controller:
$scope.model = {}
<input ng-model="model.firstProperty">
Hope this will works for you!
I came across the same issue when dealing with a non-trivial view (there are nested scopes). And finally discovered this is a known tricky thing when developing AngularJS application due to the nature of prototype-based inheritance of java-script. AngularJS nested scopes are created through this mechanism. And value created from ng-model is placed in children scope, not saying parent scope (maybe the one injected into controller) won't see the value, the value will also shadow any property with same name defined in parent scope if not use dot to enforce a prototype reference access. For more details, checkout the online video specific to illustrate this issue, http://egghead.io/video/angularjs-the-dot/ and comments following up it.
Have a look at this fiddle http://jsfiddle.net/ganarajpr/MSjqL/
I have ( I assume! ) done exactly what you were doing and it seems to be working. Can you check what is not working here for you?
Since no one mentioned this the problem can be resolved by adding $parent to the bound property
<div ng-controller="LoginController">
<input type="text" name="login" class="form-control" ng-model="$parent.ssn" ng-pattern="/\d{6,8}-\d{4}|\d{10,12}/" ng-required="true" />
<button class="button-big" type="submit" ng-click="BankLogin()" ng-disabled="!bankidForm.login.$valid">Logga in</button>
</div>
And the controller
app.controller("LoginController", ['$scope', function ($scope) {
$scope.ssn = '';
$scope.BankLogin = function () {
console.log($scope.ssn); // works!
};
}]);
For me the problem was solved by stocking my datas into an object (here "datas").
NgApp.controller('MyController', function($scope) {
$scope.my_title = ""; // This don't work in ng-click function called
$scope.datas = {
'my_title' : "",
};
$scope.doAction = function() {
console.log($scope.my_title); // bad value
console.log($scope.datas.my_title); // Good Value binded by'ng-model'
}
});
I Hop it will help
I just had this very issue using a root_controller bound to the body-element. Then I was using ng-view with the angular router. The problem is that angular ALWAYS creates a new scope when it inserts the html into ng-view element. As a consequence, my "check" function was defined on the parent scope of the scope that was modified by my ng-model element.
To solve the problem, just use a dedicated controller within route-loaded html content.
You can do that to enable search in ng-keypress enter for input text and in ng-click for the icon:
<input type="text" ng-model="searchText" ng-keypress="keyEnter(this,$event)" />
<button ng-click="check(searchText)">Check!</button>
in the controller
$scope.search = function (searchText) {
console.log(searchText);
}
$scope.keyEnter = function (serachText,$event) {
var keyCode = $event.which || $event.keyCode;
if (keyCode === 13) {//KeyCode for Enter key
console.log(searchText);
}
}
I had the same problem.
The proper way would be setting the 'searchText' to be a property inside an object.
But what if I want to leave it as it is, a string? well, I tried every single method mentioned here, nothing worked.
But then I noticed that the problem is only in the initiation, so I've just set the value attribute and it worked.
<input type="text" ng-model="searchText" value={{searchText}} />
This way the value is just set to '$scope.searchText' value and it's being updated when the input value changes.
I know it's a workaround, but it worked for me..
I was facing same problem...
The resolution that worked for me is to use this keyword..........
alert(this.ModelName);
Provide a name property to your form control