AngularJS modal dialog form object is undefined in controller

JavascriptFormsAngularjsModal Dialog

Javascript Problem Overview


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:

  1. the $scope.invitation object is not undefined in the sendInvitationController but holds the correct data, while $scope.invitationForm remains undefined.
  2. 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?

Javascript Solutions


Solution 1 - Javascript

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.

Solution 2 - Javascript

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.

Solution 3 - Javascript

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.

Solution 4 - Javascript

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.

Solution 5 - Javascript

$mdDialog.show({
	            locals: {alert:"display meassage"},
	            controller: DialogController,
	            templateUrl: 'views/dialog.html',
	            parent: angular.element(document.body),
	            clickOutsideToClose:true,
                backdrop: true,
                keyboard: true,
                backdropClick: true,

	        })

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPeteView Question on Stackoverflow
Solution 1 - JavascriptRobinView Answer on Stackoverflow
Solution 2 - JavascriptgertasView Answer on Stackoverflow
Solution 3 - Javascriptsquid314View Answer on Stackoverflow
Solution 4 - JavascriptJason SwettView Answer on Stackoverflow
Solution 5 - JavascriptJijo PauloseView Answer on Stackoverflow