AngularJs can't access form object in controller ($scope) - angularjs

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, () => {
});
}

Related

Directive with ng-model Attribute Not Resolving Using $http

Trying to make a rating directive but I'm stuck at getting rating2 to work. The first rating worked because the rating1 is hardcoded within the controller. But normally I have to get the saved rating from the db, which I'm trying to do with rating2, as u can see the value is fetched but the directive is not appearing.
https://codepen.io/eldyvoon/pen/MbBNLP
<div star-rating ng-model="rating.rating1" max="10" on-rating-select="rating.rateFunction(rating)"></div>
<br>but rating2 is actually there:
{{rating.rating2}}
<star-rating ng-model="rating.rating2" readonly="rating.isReadonly"></star-rating>
Need expert of directive to help.
Initiate rating2 :
function RatingController($http) {
this.rating1 = 5;
this.rating2 = 0; //ADD THIS LINE
var self = this;
it works for me
check here
First of all, I'm not a directive expert but i'm trying to help. I think that when html is first load, the values from db not finish execute and bind into html. The best way is not using directive instead using controller to fetch data from db.
You pass a model without rating2 into your directive and the changes from the parent controller won't affect it, because variable is created afterwards. Adding a watcher in your linker on parent scope will solve the problem;
scope.$parent.$watch('', function(rating){
updateStars();
});
Other solution would be to define a starting value in your controller.
this.rating2 = 1;
Notice that it is bad design to have a scope variable for each rating. It is cleaner to have an array of ratings and you actually do not need the watcher by doing so.
https://codepen.io/hoschnok/pen/LbJPqL
angular controller
function RatingController($http) {
this.ratings = [4];
var self = this;
$http.get('https://api.myjson.com/bins/o0r69').then(function(res){
self.ratings.push(res.data.rating2);
});
}
HTML
<div ng-app="app" ng-controller="RatingController as rating" class="container">
<div ng-repeat="r in rating.ratings">
<div star-rating ng-model="r" max="10" on-rating-select="rating.rateFunction(rating)"></div>
</div>
</div>
The watcher change handler function has parameters reversed:
//INCORRECT parameters
//scope.$watch('ratingValue', function(oldValue, newValue) {
//CORRECT parameters
scope.$watch('ratingValue', function(newValue, oldValue) {
if (newValue) {
updateStars();
}
});
The first argument of the listening function should be newValue.
The DEMO on CodePen
ALSO
The ng- prefix is reserved for core directives. See AngularJS Wiki -- Best Practices
JS
scope: {
//Avoid using ng- prefix
//ratingValue: '=ngModel',
ratingValue: '=myModel',
max: '=?', // optional (default is 5)
onRatingSelect: '&?',
readonly: '=?'
},
HTML
<!-- AVOID using the ng- prefix
<star-rating ng-if='rating' ng-model="rating.rating2"
max="10" on-rating-select="rating.rateFunction(rating)">
</star-rating>
-->
<!-- INSTEAD -->
<star-rating ng-if='rating' my-model="rating.rating2"
max="10" on-rating-select="rating.rateFunction(rating)">
</star-rating>
When a custom directve uses the name ng-model for an attribute, the AngularJS framework instantiates an ngModelController. If the directive doesn't use the services of that controller, it is best not to instantiate it.

angular directive (2-way-data-binding) - parent is not updated via ng-click

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

Why I cannot access form angular's properites in JS such as $error?

I cannot access angular's Form members. I don't have a clue why. I'm using angular 1.4. The full code is at: http://jsbin.com/lujojiduru/edit?html,js,console,output
angular.module('test',[]).controller('testController', function($scope){
$scope.sendInvitations = function(){
var error = myForm.NewInvitations.$error;
console.log('sent');
console.log('value: ' + error );
};
});
the value of $error is always undefined. Any idea?
var error = myForm.NewInvitations.$error;
should be:
var error = $scope.myForm.NewInvitations.$error;
Notice the $scope
This is assuming you have the name="myForm" on your <form> tag
So something like:
<div ng-controller="testController">
<form name="myForm" novalidate>
...
</form>
</div>
you can also, if you prefer, send in the validity of your form, to your method on the controller.
So:
<button class="btn btn-success" ng-click="sendInvitations(myForm.$valid)">Send Invitations</button>
And in your controller:
$scope.sendInvitations = function(isValid){
if(!isValid)
return;
};
Update
You also don't have any errors.
Add required to your input.
So your controller is now:
angular.module('test',[]).controller('testController', function($scope){
$scope.sendInvitations = function(){
debugger;//
var error = $scope.myForm.NewInvitations.$error;
console.log('sent');
console.log(error );
};
});
and your input
<input style="width: 95%" name="NewInvitations" type="email" ng-model="newInvitations" required />
This is an update to your bin
http://jsbin.com/hebikucanu/1/edit?html,js,console,output
myForm is not accessible in the global scope. You can pass it in as an argument to sendInvitations.
ng-click="sendInvitations(myForm)
$scope.sendInvitations = function(myForm){
It's unlikely that you'd need to do this, though. You can do use the myForm properties in the view.
In angular, you should never touch the DOM. Meaning, you should never invoke HTML elements directly as you would in traditional HTML/JS settings. (read more)
Angular encourages the use of ng-model to employ two-way binding as a means to communicate between the controller and the view. (angular two-way data binding tutorial)
Thus, the first change you should attempt is to replace
var error = myForm.NewInvitations.$error;
with:
var error = $scope.NewInvitations;
This will cause the code to run.
But it appears that you want to retrieve the email input validation error and display it through angular.
Here is an excellent explanation with a tutorial that I think achieves what you're trying to do. If you just want to see the code in action, try this link. (Be sure to hit the Run button.)
Hope this helps!

How to initialize form as scope variable in controller

I have a simple angular form
<form name="myForm" ng-controller="myFormController"></form>
and need to call $setPristine() to myForm in myFormController. What is the best way to initialize this form as a $scope variable?
I tried $scope.myForm.$setPristine(); but it gave me:
Cannot read property '$setPristine' of undefined
Thanks in advance.
EDIT From the docs:
name (optional)
Name of the form. If specified, the form controller will be published into related scope, under this name.
That means you can access it in a controller, but how?
form directive does publish the name of the form to the scope. But if the form is nested inside the ng-controller element, then the form's scope variable is not yet available when the controller function runs.
As an illustration:
<div ng-controller="OuterCtrl">
<form name="form1">
<div ng-controller="InnerCtrl"></div>
</form>
</div>
The following would happen:
.controller("OuterCtrl", function($scope){
// $scope.form1.$setPristine(); // this will fail
})
.controller("InnerCtrl", function($scope){
$scope.form1.$setPristine(); // this will succeed
});
It is rarely needed to access the form when the controller function runs. Typically, it's done in response to some action, like a submit action. When that happens, the "OuterCtrl" will have $scope.form1:
.controller("OuterCtrl", function($scope){
$scope.submitForm = function(){
//... do something with form data
$scope.form1.$setPristine();
}
});
In that respect, $timeout would, in fact, work and would not cause race conditions. But you should re-examine why you need it the form when the controller function first runs.
Try this in your controller.
$timeout(function () {
// here you should be able to access $scope.myForm
})
I just wasted some time dealing with this. In my case, I had the form shown a bit later using a ng-if. Changing the form conditions to an ng-show resolved the non-initialization of the $scope.myform.

ng-form nested within ng-switch

I ran into an issue with ng-form not setting up the form on scope when its nested within an ng-scope.
For example
<div ng-controller='TestCtrl'>
<ng-switch on="switchMe">
<div ng-switch-default>Loading...</div>
<div ng-switch-when="true">
<form name="nixTest">
<input placeholder='In switch' ng-model='dummy'></input>
<button ng-click="test()">Submit</button>
</form>
</div>
</ng-switch>
</div>
Controller:
controllers.TestCtrl = function ($scope) {
$scope.switchMe = true;
$scope.test = function () {
if ($scope.nixTest) {
alert('nixTest exists')
} else {
alert('nixTest DNE')
}
}
}
Are there any work arounds to this ? Test fiddle can be found here
ng-switch creates a child scope and the form is created on this scope. Hence the child scope form would not be available on the parent scope.
To get access to it, you can pass it to the method test() like ng-click=test(nixTest). So the scope method signature would also need to updated to support the input parameter.
I ran into the same issue. Unfortunately I could not easily apply Chandermani's solution, because I need to access the form name from an $on call, within the ng-switch parent scope.
Thus, I resorted to creating a directive that sends the form's name to the $rootScope:
.directive("globalName", ["$rootScope", function($rootScope) {
return function(scope, element) {
$rootScope["get_" + element.attr('name')] = function() {
return scope[element.attr('name')];
};
}
}]);
Usage is like this:
<form name="whatever" novalidate global-name>...</form>
and then you access the form in controllers e.g. like this:
$scope.get_whatever().$setPristine();
$scope.get_whatever().$setUntouched();
Being the name in the $rootScope, it does not depend anymore on your DOM structure.
I understand this is not an optimal solution, as it pollutes the global namespace, but either I feel uncomfortable with form name visibility depending on the DOM structure, in somewhat unexpected ways.

Resources