Search code examples
javascriptangularjsangularjs-ng-model

How do I make ng-model-options use dynamic variables?


I am having an issue where ng-model-options isn't reflecting the changes I need it to. For example, in the snippet below if you enter 4:00 pm into both time inputs you'll see the UTC output is different - the first is 6 and the second is 8. This is expected. However, the issue occurs when I select +0800 using the dropdown. Since this updates the timezone variable, both time inputs should now display 8 when I enter 4:00 pm since the first input should now use the timezone variable (as specified in its ng-model-options). However this isn't happening. Even after I clear the input and re-enter the time manually it still shows the incorrect time. How can I make the timezone option in ng-model-options use a dynamically changing variable such as timezone?

See issue below:

angular.module('myApp', [])
  .controller('myController', function($scope) {
    $scope.timezones = ['+1000', '+0800'];
    $scope.timezone = $scope.timezones[0];

    $scope.time = '';
    $scope.eightTime = '';
  });

angular.element(document).ready(() => {
  angular.bootstrap(document, ['myApp']);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.11/angular.min.js"></script>
<div ng-controller="myController">
  <select ng-model="timezone">
    <option ng-repeat="timezone in timezones" ng-value="timezone">{{timezone}}</option>
  </select>
  <p>Selected Timezone: {{timezone}}</p>

  <input type="time" ng-model="time" ng-model-options='{timezone: timezone}' />
  <p>Using dropdown T.Z of '{{timezone}}': {{time.getUTCHours()}}</p>

  <input type="time" ng-model="eightTime" ng-model-options="{timezone: '+0800'}">
  <p>Hardcoded '+0800': {{eightTime.getUTCHours()}}</p>
  <!-- ^^^ This should be the output when '+0800' is selected in the dropdown -->
</div>


Solution

  • As per the documentation -

    The ngModelOptions expression is only evaluated once when the directive is linked; it is not watched for changes. However, it is possible to override the options on a single ngModel.NgModelController instance with NgModelController#$overrideModelOptions()

    I have changed some of the lines to make it work for you :)

    angular.module('myApp', [])
      .controller('myController', function($scope) {
        $scope.timezones = ['+1000', '+0800'];
        $scope.timezone = $scope.timezones[0];
        $scope.time = '';
        $scope.eightTime = '';
        
        $scope.$watch('timezone',function(v){
            $scope.time = '';
            $scope.myForm.time.$overrideModelOptions({'timezone': $scope.timezone}); 
            //this will update the options whenever the timezone will be changed.
        })
        
      });
    
    angular.element(document).ready(() => {
      angular.bootstrap(document, ['myApp']);
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
    <div ng-controller="myController">
      <form name="myForm">
        <select ng-model="timezone" ng-options="timezone for timezone in timezones">
        </select> <!-- ng-options is the correct way to provide options to the select dropdown -->
        <p>Selected Timezone: {{timezone}}</p>
    
        <input type="time" name="time" ng-model="time" ng-model-options='{timezone: timezone}' />
        <p>Using dropdown T.Z of '{{timezone}}': {{time.getUTCHours()}}</p>
        <input type="time" ng-model="eightTime" ng-model-options="{timezone: '+0800'}">
        <p>Hardcoded '+0800': {{eightTime.getUTCHours()}}</p>
        <!-- ^^^ This should be the output when '+0800' is selected in the dropdown -->
      </form>
    </div>

    You will find more details about $overrideModelOptions here - https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$overrideModelOptions

    Edited:

    You can achieve it by creating a separate directive.

    **For versions >1.6.2 **

    angular.module('myApp', ['kcd.directives'])
      .controller('myController', function($scope) {
        $scope.timezones = ['+1000', '+0800'];
        $scope.timezone = $scope.timezones[0];
        $scope.time = '';
        $scope.eightTime = '';
        
    });
    angular.module('kcd.directives', []).directive('kcdRecompile', ['$parse', function($parse) {
      'use strict';
      return {
        transclude: true,
        link: function link(scope, $el, attrs, ctrls, transclude) {
          var previousElements;
    
          compile();
    
          function compile() {
            transclude(scope, function(clone, clonedScope) {
              // transclude creates a clone containing all children elements;
              // as we assign the current scope as first parameter, the clonedScope is the same
              previousElements = clone;
              $el.append(clone);
            });
          }
    
          function recompile() {
            if (previousElements) {
              previousElements.remove();
              previousElements = null;
              $el.empty();
            }
    
            compile();
          }
    
          scope.$watch(attrs.kcdRecompile, function(_new, _old) {
            var useBoolean = attrs.hasOwnProperty('useBoolean');
            if ((useBoolean && (!_new || _new === 'false')) || (!useBoolean && (!_new || _new === _old))) {
              return;
            }
            // reset kcdRecompile to false if we're using a boolean
            if (useBoolean) {
              $parse(attrs.kcdRecompile).assign(scope, false);
            }
            recompile();
          }, typeof $parse(attrs.kcdRecompile)(scope) === 'object');
        }
      };
    }]);
    
    angular.element(document).ready(() => {
      angular.bootstrap(document, ['myApp']);
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.0/angular.min.js"></script>
    <div ng-controller="myController">
      <div kcd-recompile="timezone">
        <select ng-model="timezone" ng-options="timezone for timezone in timezones">
        </select> <!-- ng-options is the correct way to provide options to the select dropdown -->
        <p>Selected Timezone: {{timezone}}</p>
    
        <input type="time" name="time" ng-model="time" ng-model-options="{timezone: timezone}"/>
        <p>Using dropdown T.Z of '{{timezone}}': {{time.getUTCHours()}}</p>
        <input type="time" ng-model="eightTime" ng-model-options="{timezone: '+0800'}">
        <p>Hardcoded '+0800': {{eightTime.getUTCHours()}}</p>
        <!-- ^^^ This should be the output when '+0800' is selected in the dropdown -->
      </div>
    </div>

    Reference Post - Dynamically Setting ngModelOptions in Angular