AngularJS modal dialog form object is undefined in controller
We have a page that opens a modal dialog with a form like below. However when we hit the controller that should handle the form action, the form object is undefined and I am too much of an Angular newbie to understand why...
This is the parent page controller holds the function to open the modal dialog:
app.controller('organisationStructureController', ['$scope', ..., '$modal', function ($scope, ..., $modal) {
$scope.openInvitationDialog = function (targetOrganisationId) {
$modal.open({
templateUrl: 'send-invitation.html',
controller: 'sendInvitationController',
resolve: {$targetOrganisationId: function () {
return targetOrganisationId;
}
}
}
);
};
on a page like this:
// inside a loop over organisations
<a ng-click="openInvitationDialog({{organisation.id}})">Invite new member</a>
the invitation-dialog html looks like this:
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<!-- ... -->
</div>
<div class="modal-body">
<form name="invitationForm">
<div class="form-group">
<label for="email" style="color:white;">Email</label>
<input type="email" class="form-control" autocomplete="off" placeholder="New member email" id="email" name="email" ng-model="invitation.email" required="true"/>
<span class="error animated fadeIn" ng-show="invitationForm.email.$dirty && invitationForm.email.$error.required">Please enter an email address!</span>
<span class="error animated fadeIn" ng-show="invitationForm.email.$error.email">Invalid email</span>
</div>
<!-- ... -->
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-click="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary" ng-click="sendInvitation()">Invite</button>
</div>
</form>
</div>
</div>
</div>
The controller that should handle the invitation is somewhere else:
app.controller('sendInvitationController', ['$targetOrganisationId', '$scope', ...,
function ($targetOrganisationId, $scope, ...) {
$scope.invitation = {
// ...
targetOrganisation: {
id: $targetOrganisationId
}
};
$scope.sendInvitation = function () {
// $scope.invitationForm is undefined
if ($scope.invitationForm.$invalid) {
return false;
}
// send the invitation...
};
}]);
So what's the correct way to get the form scope into the controller?
Maybe I need to inject $modal
into the sendInvitationController
and add the sendInvitation
function to it? But when I do that the action never enters the controller. Or do I have to add the function that handles the submit action to $modal.open({ ...
instead of referencing the controller? Though I'd much prefer to have the sendInvitationController in its own file and scope.
Thanks for any help!
EDIT
We found several things that helped us build a workaround and might help someone answer the question itself:
- the
$scope.invitation
object is not undefined in thesendInvitationController
but holds the correct data, while$scope.invitationForm
remains undefined. - inside the send-invitation.html we can access
$scope.invitationForm.$invalid
and do the validation right there:<button type="button" ng-click="sendInvitation()" ng-disabled="invitationForm.$invalid">Invite</button>
So the question is: why does the binding of the invitationForm
object to the $scope
fail on submit while the form model binds correcetly?
I had the same issue and could solve it by defining the form object in the scope of the modals controller. To get your code working put, for example, $scope.form = {};
in the beginning of your controller and change your form tag to <form name="form.invitation">
. Afterwards $scope.form.invitation.$invalid
should be filled.
Update Nov 2014: starting from angular-ui-bootstrap 0.12.0
transclusion scope is merged with the controller's scope. There is no need to do anything.
Before 0.12.0:
To put invitationForm
directly in your parent controller scope you need to bypass transcluded scope this way:
<form name="$parent.invitationForm">
Above will automaticaly create form object in your parent controller. No need for pre-initialization stuff, long object paths or passing by event. Just access it with $scope.invitationForm
once modal is opened.
The answer to the question of "Why?" is "scoping". tl;dr you created a new scope with the modal dialog which hid the scope's form object from your controller.
If we simplify your code, we roughly get the following:
<div ng-ctrl="organizeCtrl">
<modal-dialog>
<form name="invitationForm">
<input type="email" ng-model="invitation.email" placeholder="Enter email..." />
<input type="submit" ng-click="sendInvitation()" text="Invite!" />
<input type="button" ng-click="cancel()" text="Cancel :(" />
</form>
</modal-dialog>
</div>
(This is a very simplified version which should still have all of the core components.) Now, let's look at where scopes are created and what is injected in them.
<div ng-ctrl="sendInvitationController">
<!-- scope created above with "invitation" and "sendInvitation" from sendInvitationController -->
<modal-dialog>
<!-- scope created above for the modal dialog transclude -->
<form name="invitationForm">
<!-- add "invitationForm" to the modal dialog's scope -->
<input type="email" ng-model="invitation.email" placeholder="Enter email..." />
<input type="submit" ng-click="sendInvitation()" text="Invite!" />
<input type="button" ng-click="cancel()" text="Cancel :(" />
</form>
</modal-dialog>
</div>
Here, you can see that there is a new child scope created at the <modal-dialog>
element and that is where the invitationForm
object is actually added. That is why you can't see the object in the sendInvitationController
but you can see it on the buttons for ng-disabled
. If you want to be able to access the form construct outside of the <modal-dialog>
element (e.g. in the sendInvitationController
) you will need to pass that in the function call:
<div ng-ctrl="organizeCtrl">
<modal-dialog>
<form name="invitationForm">
<input type="email" ng-model="invitation.email" placeholder="Enter email..." />
<input type="submit" ng-click="sendInvitation(invitationForm)" text="Invite!" />
<input type="button" ng-click="cancel()" text="Cancel :(" />
</form>
</modal-dialog>
</div>
With the controller accepting the invitation form as a parameter to the sendInvitation
function:
app.controller('sendInvitationController', ['$targetOrganisationId', '$scope', ...,
function ($targetOrganisationId, $scope, ...) {
$scope.invitation = {
targetOrganisation: {
id: $targetOrganisationId
}
};
$scope.sendInvitation = function (form) {
if (form.$invalid) {
return false;
}
// send the invitation...
};
}]);
@Robin identified the other solution, specifically to create an object rooted in the scope of the sendInvitationController
and then attach the form directly to that object, relying on Angular's scope traversal mechanism to find the form
object on the scope outside of the <modal-dialog>
and attach the form object to that. Note that if you did not specify $scope.form = {}
in the sendInvitationController
, Angular would have created a new object for form
on the scope for the <modal-dialog>
and you still would not have been able to access it in the sendInvitationController
.
Hopefully this helps you or other people learning about Angular scoping.
I got mine to work like this:
$modal.open({
templateUrl: 'send-invitation.html',
controller: 'sendInvitationController',
scope: $scope // <-- I added this
}
No form name, no $parent
. I'm using AngularUI Bootstrap version 0.12.1.
I was tipped off to this solution by this.