Jeff
Jeff

Reputation: 36583

AngularJS ng-model on directive template

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>

Upvotes: 1

Views: 1167

Answers (1)

New Dev
New Dev

Reputation: 49600

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

Upvotes: 1

Related Questions