80

I'm having trouble understanding/using the scopes for an angular UI modal.

While not immediately apparent here, I have the modules and everything set up correctly (as far as I can tell), but these code samples in particular are where I'm finding the bug.

index.html (the important part of it)

<div class="btn-group">
    <button class="btn dropdown-toggle btn-mini" data-toggle="dropdown">
        Actions
        <span class="caret"></span>
    </button>
    <ul class="dropdown-menu pull-right text-left">
        <li><a ng-click="addSimpleGroup()">Add Simple</a></li>
        <li><a ng-click="open()">Add Custom</a></li>
        <li class="divider"></li>
        <li><a ng-click="doBulkDelete()">Remove Selected</a></li>
    </ul>
</div>

Controller.js (again, the important part)

MyApp.controller('AppListCtrl', function($scope, $modal){
    $scope.name = 'New Name';
    $scope.groupType = 'New Type';

    $scope.open = function(){
        var modalInstance = $modal.open({
            templateUrl: 'partials/create.html',
            controller: 'AppCreateCtrl'
        });
        modalInstance.result.then(function(response){

            // outputs an object {name: 'Custom Name', groupType: 'Custom Type'}
            // despite the user entering customized values
            console.log('response', response);

            // outputs "New Name", which is fine, makes sense to me.                
            console.log('name', $scope.name);

        });
    };
});

MyApp.controller('AppCreateCtrl', function($scope, $modalInstance){
    $scope.name = 'Custom Name';
    $scope.groupType = 'Custom Type';

    $scope.ok = function(){

        // outputs 'Custom Name' despite user entering "TEST 1"
        console.log('create name', $scope.name);

        // outputs 'Custom Type' despite user entering "TEST 2"
        console.log('create type', $scope.groupType);

        // outputs the $scope for AppCreateCtrl but name and groupType
        // still show as "Custom Name" and "Custom Type"
        // $scope.$id is "007"
        console.log('scope', $scope);

        // outputs what looks like the scope, but in this object the
        // values for name and groupType are "TEST 1" and "TEST 2" as expected.
        // this.$id is set to "009" so this != $scope
        console.log('this', this);

        // based on what modalInstance.result.then() is saying,
        // the values that are in this object are the original $scope ones
        // not the ones the user has just entered in the UI. no data binding?
        $modalInstance.close({
            name: $scope.name,
            groupType: $scope.groupType
        });
    };
});

create.html (in its entirety)

<div class="modal-header">
    <button type="button" class="close" ng-click="cancel()">x</button>
    <h3 id="myModalLabel">Add Template Group</h3>
</div>
<div class="modal-body">
    <form>
        <fieldset>
            <label for="name">Group Name:</label>
            <input type="text" name="name" ng-model="name" />           
            <label for="groupType">Group Type:</label>
            <input type="text" name="groupType" ng-model="groupType" />
        </fieldset>
    </form>
</div>
<div class="modal-footer">
    <button class="btn" ng-click="cancel()">Cancel</button>
    <button class="btn btn-primary" ng-click="ok()">Add</button>
</div>

So, my question stands: why is the scope not being double-bound to the UI? and why does this have the customized values, but $scope does not?

I have tried to add ng-controller="AppCreateCtrl" to the body div in create.html, but that threw an error: "Unknown provider: $modalInstanceProvider <- $modalInstance" so no luck there.

At this point, my only option is to pass back an object with this.name and this.groupType instead of using $scope, but that feels wrong.

Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
coblr
  • 3,008
  • 3
  • 23
  • 31

5 Answers5

66

When nested scopes are involved, do not bind <input>s directly to members of the scope:

<input ng-model="name" /> <!-- NO -->

Bind them to at least a level deeper:

<input ng-model="form.name" /> <!-- YES -->

The reason is that scopes prototypically inherit their parent scope. So when setting 1st level members, these are set directly on the child scope, without affecting the parent. In contrast to that, when binding to nested fields (form.name) the member form is read from the parent scope, so accessing the name property accesses the correct target.

Read a more detailed description here.

Nikos Paraskevopoulos
  • 39,514
  • 12
  • 85
  • 90
  • 14
    While changing this to 'form.name' didn't do anything, changing it to ng-model="$parent.name" fixed the problem. Thanks! (and thanks for the reading material as well. Haven't seen this one yet.) – coblr Sep 20 '13 at 20:54
  • 1
    If you use the `controller as` syntax then you won't run into nested scope issues like this – rob Jan 06 '14 at 21:24
  • What's the ```controller as ``` syntax? – adrianboimvaser Jan 16 '14 at 16:21
  • 1
    See http://docs.angularjs.org/api/ng.directive:ngController. Basically doing ng-controller="MyCtrl as my" then referencing a ng-model like my.someVar – Ryan Q Jan 20 '14 at 19:26
  • 2
    @Nikos Paraskevopoulos, What does "form.name" represent? It's not mentioned in provided code at all! – ŁukaszBachman Jun 05 '14 at 07:08
  • It is there to demonstrate the fact that you need to put the variable bound to `ng-model` under some other object, not directly under the scope. – Nikos Paraskevopoulos Jun 05 '14 at 07:24
  • To make validation work inside the modal too, see my answer: http://stackoverflow.com/questions/20713633/accessing-angular-bootstrap-modal-form-from-controller/24452687#24452687 – Zymotik Jun 27 '14 at 13:16
  • @fractalspawn Based on your comment, I'm confused as to whether this answer actually worked for you. Did it? I don't understand where `form` would come from. I don't even understand how to apply this solution. – Jason Swett May 02 '15 at 13:50
  • No this answer didn't work for me as it was provided. Based on the reading material Nikos put up, I implemented something that ended up being closer to what gertas points out later the next year (jul '14). This was in Sept '13, before AUI 0.12 (as gertas pointed out) so I wasn't able to accept his answer originally. Your answer is the next gen so I will accept yours now to keep things current ;) No worries on needing to apply this anymore. Thanks for the update. – coblr May 03 '15 at 18:02
59

I got mine to work like this:

var modalInstance = $modal.open({
  templateUrl: 'partials/create.html',
  controller: 'AppCreateCtrl',
  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.

Jason Swett
  • 43,526
  • 67
  • 220
  • 351
  • This is a much better solution. Much cleaner. We just recently upgraded to 0.12.1 and are currently fixing the breaking changes it introduced. I can add this to the list. Thanks!! – coblr May 03 '15 at 17:48
  • I'm accepting this answer because it's the current implementation and will be more useful to those who end up here trying to get things to work. Please read the full thread unless you are using the latest (~0.12) version of Angular UI. – coblr May 03 '15 at 18:04
  • The modal doesn't seem to be closable with $modalStack if the state has changed if the scope is set – phazei Dec 18 '15 at 17:45
  • Hi, this solved my problem for a while, but noticed a weird thing when closing the modal/dialog, content was changing and it seemed to mess with something on a scope object. adding scope: $scope, preserveScope: true //<-- important somehow helped! not entire sure why, but found it on https://material.angularjs.org/latest/api/service/$mdDialog#javascript-promise-api-syntax-custom-dialog-template Edit: I found out why. when you set scope: for the dialog, and then close the dialog "This scope will be destroyed when the bottom sheet is removed unless preserveScope is set to true." – Max Sep 27 '16 at 12:18
  • In my controller, I am using vm=this. I am not using $scope. Then what should I assign to scope? – Ankit Prajapati Apr 06 '18 at 21:49
7

Update Nov 2014:

Actually your code should work after upgrading to ui-bootstrap 0.12.0. Transcluded scope is merged with controller's scope so no more need for $parent or form. stuff.

Before 0.12.0:

The modal uses transclusion to insert its contents. Thanks to ngForm you can control the scope by name attribute. So to escape transcluded scope just modify the form this way:

<form name="$parent">

or

<form name="$parent.myFormData">

The model data will be available in controller scope.

gertas
  • 16,869
  • 1
  • 76
  • 58
  • To be clear, are you saying that the scope of the controller inside of which you're calling `$modal` should be available to the controller assigned to the modal? – Jason Swett May 02 '15 at 14:07
  • No, the problem was that even modal instance controller wasn't easily accessible for forms. To access controller which opens the modal just put `scope:$scope` in $modal.open params map. – gertas May 04 '15 at 11:58
  • Do you mean Angular UI? Is that different from UI Bootstrap? – Noah May 22 '15 at 08:36
1
$scope.open = function () {

          var modalInstance = $uibModal.open({
              animation: $scope.animationsEnabled,
              templateUrl: 'myModalContent.html',
              controller: 'salespersonReportController',
              //size: size
              scope: $scope
            });

      };

it works for me scope: $scope thank u Jason Swett

cracker
  • 4,900
  • 3
  • 23
  • 41
ali Shoaib
  • 11
  • 1
1

I add scope:$scope then it works .Cool

JBRandri
  • 295
  • 2
  • 9