1

I have the following directive:

.directive('radioList', function ($ionicModal) {
    return {
        restrict: 'A',

        scope: {
            selectedItem: '=selectedItem',
            items: '=items'
        }
    };

with the following template html:

    <div ng-repeat="item in items">
        <ion-radio ng-model="selectedItem" ng-value="item">{{item.toString()}}</ion-radio>
    </div>

Although bound in the directive, ng-model does not update properly on the directive isolated scope or on the parent scope.

If I do this:

.directive('radioList', function ($ionicModal) {
    return {
        restrict: 'A',

        scope: {
            selectedItemInternal: '=selectedItem',
            items: '=items'
        },
        link: function (scope, element, attr) {
            Object.defineProperty(scope, 'selectedItem', {
                get: function () {
                    return scope.selectedItemInternal;
                },
                set: function (value) {
                    scope.selectedItemInternal = value;
                }
            });
        }
    };

Everything works fine and my setter for selectedItems is called.

Seems like a bug in Angular?

UPDATE: Here is my total, full directive:

.directive('radioList', function ($ionicModal) {

    return {
        restrict: 'A',

        scope: {
            selectedItemInternal: '=selectedItem',
            items: '=items',
            header: '=header',
            showCancel: '=showCancel',
            listHide: '&listHide',
            doneText: '=doneText'
        },

        link: function (scope, element, attr) {

            element.css('cursor', 'pointer');

            var modal;

            scope.hide = function (result) {
                modal.remove();
                modal = null;
                if (scope.listHide) {
                    scope.listHide()(result, scope.selectedItem);
                }
            };

            // allow deselecting a radio button
            var isDeselecting = false; // event fires again after scope.selectedItem changes
            var hasChanged = false;
            scope.click = function (event) {
                if (!isDeselecting) {
                    hasChanged = scope.selectedItem != angular.element(event.target).scope().item;
                    isDeselecting = true;
                } else {
                    if (!hasChanged) {
                        scope.selectedItem = null;
                    }
                    isDeselecting = false;
                    hasChanged = false;
                }
            };

            // required to handle that click only fires once when double clicking
            scope.doubleClick = function () {
                isDeselecting = false;
                hasChanged = false;
            };

            // necessary due to a bug in AngularJS binding ng-model in the template
            Object.defineProperty(scope, 'selectedItem', {
                get: function () {
                    return scope.selectedItemInternal;
                },
                set: function (value) {
                    scope.selectedItemInternal = value;
                }
            });

            element.on('click', function () {

                $ionicModal.fromTemplateUrl('templates/radio-list.html', {
                    scope: scope
                }).then(function (m) {
                    modal = m;

                    // due to bug in ionic framework, scroll won't work unless we do this
                    ionic.keyboard.hide();

                    modal.show();
                });

            });

            element.on('$destroy', function () {
                if (modal) {
                    modal.remove();
                    modal = null;
                }
            });

        }
    };
})

Here is my radio-list.html

<ion-modal-view>

    <div class="text-center" ng-show="header">
        <h5>{{header}}</h5>
    </div>

    <ion-content style="position: absolute; top: {{showCancel ? '30px': '0'}}; bottom: {{showCancel ? 103 : 53}}px; border: 1px grey;border-bottom-style: solid; width: 100%;">
        <div ng-repeat="item in items">
            <ion-radio ng-click="click($event)" ng-dblclick="doubleClick()" ng-model="selectedItem" ng-value="item">{{item.toString()}}</ion-radio>
        </div>
    </ion-content>

    <a class="button button-full button-energized" ng-show="showCancel"
       style="position: absolute; bottom: 50px; width: 100%; margin-top: 2px; margin-bottom: 2px;"
       ng-click="$event.stopPropagation();hide(false)">Cancel</a>

    <a class="button button-full button-energized"
       style="position: absolute; bottom: 0; width: 100%; margin-top: 2px; margin-bottom: 2px;"
       ng-click="$event.stopPropagation();hide(true)">{{doneText || 'Done'}}</a>

</ion-modal-view>

and here is the usage:

                <label class="item item-input validated" radio-list items="locations"
                       selected-item="account.locationPreference">
                    <span class="input-label">LOCATION</span>
                    <input type="hidden" ng-model="account.locationPreference" name="locationPreference"
                           required="required">
                    <span ng-show="account && !account.locationPreference"
                          class="placeholder value">Neighborhood</span>

                    <span class="input-value">{{account.locationPreference}}</span>
                </label>
Jeff
  • 35,755
  • 15
  • 108
  • 220
  • 2
    You are probably invoking the directive as ``. Note the `xxx`! Two-way binding will not work in "top-level" properties; place the `xxx` under an object (e.g. a controller with the `controller as` syntax): ``. – Nikos Paraskevopoulos Apr 15 '15 at 16:07
  • Is this documented somewhere? – Jeff Apr 15 '15 at 16:28
  • Also - just verified - selected-item="account.locationPreference"...so it's not top level – Jeff Apr 15 '15 at 16:30
  • Hi again. I won't get into detail about the "top level" thing as it is obviously not the problem (keep it in mind though). Could you perhaps provide a fiddle that demonstrates the problem? – Nikos Paraskevopoulos Apr 15 '15 at 20:15

1 Answers1

1

Read up on scope inheritance and somewhat unintuitive behavior that results when you set a value, like what ng-model does.

In this case, ng-repeat creates a child scope for each iteration, so when you have

ng-model="selectedItem"

you are creating selectedItem property on the child scope and setting the value there - not on the directive's isolate scope.

As a quick fix, you could set the $parent.selectedItem directly:

<div ng-repeat="item in items">
  <ion-radio ng-model="$parent.selectedItem" ng-value="item">{{item.toString()}}
  </ion-radio>
</div>

Demo 1

Alternatively, you could use the bindToController and controllerAs syntax:

return {
  // ...
  scope: {
    // whatever you have
  },
  bindToController: true,
  controllerAs: vm,
  controller: angular.noop
}

and use the alias in the template:

<div ng-repeat="item in vm.items">
  <ion-radio ng-model="vm.selectedItem" ng-value="item">{{item.toString()}}
  </ion-radio>
</div>

Demo 2

Community
  • 1
  • 1
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • but why does it work as a property on the controller with a getter and setting but not without? – Jeff Apr 16 '15 at 10:55
  • @Jeff, it *does* work without it as in the first demo. All the `controllerAs` is doing is that it adds an alias as a property of the scope - `scope.vm === ctrl` (if `ctrl` is the controller function), which forces a dot notation `vm.prop`, to avoid the prototypical inheritance issue stated above. – New Dev Apr 16 '15 at 16:04
  • Still not working for me....I've updated the full directive and html as an example – Jeff Apr 16 '15 at 16:42
  • @Jeff, can you try to reduce the code you posted to isolate the problem? Remove everything that has nothing to do with the issue. Or at very least, please create a plunker that reproduces your issue – New Dev Apr 16 '15 at 16:56
  • I've tried but can't seem to. It could be the order in which I set it in the controller? Or something like that? Does Angular treat properties with gets/sets differently than regular properties? Or does it only work because my property on the scope captures the exact parent scope of the directive via closure? – Jeff Apr 16 '15 at 18:31
  • @Jeff, are you talking about the `Object.defineProperty`? Why do you even have that? – New Dev Apr 16 '15 at 18:33
  • Please see original post...THAT is what makes it work. – Jeff Apr 16 '15 at 19:48
  • @Jeff, you claimed that, yes, but I don't think it's true, and it seems completely unnecessary. You should be able to achieve the same with `scope: { selectedItem: "=", }` - no need for `selectedItemInternal`. But, regardless, it shouldn't matter. Have you tried adding `$parent`: `ng-model="$parent.selectedItem"`? – New Dev Apr 16 '15 at 20:25
  • I can absolutely guarantee you that with the property and internalSelectedItem attribute it works and without it does not. Yes, I tried adding $parent and it doesn't work. I also looked in the debugger at the time the property setter is invoked and saw that scope.$parent is in fact the controller's scope (not the directive or anything else like that) – Jeff Apr 16 '15 at 21:30
  • @Jeff, ah, and it seems that `` also creates a child scope, necessitating something like `$parent.$parent`. That is why then `$parent`-approach is not recommended, and `controllerAs` approach is more resilient to DOM changes – New Dev Apr 16 '15 at 21:41
  • @Jeff, ok, so I recreated a plunker with your code and my answer with `controllerAs` approach - works fine without getter/setter property: http://plnkr.co/edit/KQuogQiC34gRs1t0jkjy?p=preview – New Dev Apr 16 '15 at 22:08
  • where is the vm object defined? – Jeff Apr 17 '15 at 12:28
  • And where do you set '=selectedItem' for the parent of the directive? – Jeff Apr 17 '15 at 12:28
  • @Jeff, the `vm` is the controller alias - `controllasAs: "vm"` - this publishes the controller instance on the scope under `vm`. I don't understand the second question - `selectedItem` is an isolate scope variable: `scope: {selectedItem: "="}` – New Dev Apr 17 '15 at 12:46
  • In your example, ng-model="vm.selectedItem"....but selectedItem is put on the directive scope...not on vm...so maybe I don't understand something – Jeff Apr 17 '15 at 13:34
  • 1
    Read about [`bindToController`](https://docs.angularjs.org/api/ng/service/$compile#-bindtocontroller-) – New Dev Apr 17 '15 at 13:48
  • Even with bindToController the selectedItem: '=' and items: '=' ends up on the directive's scope, not the scope.vm – Jeff Apr 17 '15 at 14:46
  • @Jeff, comments are not to learn the framework nuances from scratch or to debug your code. It's here just to comment on the answer, and the answer (as I showed in the plunker) should address your original concern. I strongly suggest you read more about isolate scope and `bindToController`, and prototypical inheritance, and if you have further questions, please create a small illustrative example and inquire about it in a new question. – New Dev Apr 17 '15 at 16:19
  • Understood. So, question goes unanswered. – Jeff Apr 17 '15 at 17:52
  • @Jeff, I'm sure glad I spent the time trying to help. I gave the answer, complete with illustrative examples. I showed that it is working with your own code in a plunker - something you should have done, at least, as a matter of courtesy. I can't understand what doesn't work in whatever you are trying to do if you can't reproduce it in a plunker – New Dev Apr 17 '15 at 19:04