在 Angular 中,范围变量 *before* ngModel 绑定输入的“值”的绑定属性

In Angular, bind attribute from scope variable *before* ngModel binds the input’s `value`

在我的 Angular 应用程序中,我定义了一个包含 <input type="range"> 的自定义滑块指令。我的自定义指令支持 ngModel 将滑块的值绑定到一个变量。自定义指令还需要一个 fraction-size 属性。它对值进行计算,然后使用结果来设置包装的 <input>.

step

我在组合这两个功能时看到一个错误 – ngModel 和我的绑定属性值。它们 运行 顺序错误。

这里有一个演示:

angular.module('HelloApp', []);

angular.module('HelloApp').directive('customSlider', function() {
    var tpl = "2 <input type='range' min='2' max='3' step='{{stepSize}}' ng-model='theNum' /> 3";
    
    return {
        restrict: 'E',
        template: tpl,
        require: 'ngModel',
        scope: {
            fractionSize: '='
        },
        link: function(scope, element, attrs, ngModelCtrl) {
            scope.stepSize = 1 / scope.fractionSize;
            
            scope.$watch('theNum', function(newValue, oldValue) {
                ngModelCtrl.$setViewValue(newValue);
            });
            
            ngModelCtrl.$render = function() {
                scope.theNum = ngModelCtrl.$viewValue;
            };
        }
    };
});

angular.module('HelloApp').controller('HelloController', function($scope) {
 $scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>

<div ng-app="HelloApp" ng-controller="HelloController">
    <h3>Custom slider</h3>
    
    <custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
    
    <h3>View/edit the slider’s value</h3>
    <input ng-model="someNumber"></input>
</div>

上面的滑块应该从中间开始,代表 2.5。但它实际上一直从右边开始(代表 3)。如果您拖动滑块,或者如果您通过编辑文本字段更改其绑定值,滑块会自行修复并允许值为 2.5。

我已经弄清楚为什么会在演示中发生这种情况——我只是不知道如何解决它。目前,当一个新的自定义滑块被动态添加到页面时,包装的 <input>step 是未定义的,并且默认为 1。然后 ngModel 将值设置为 2.5 – 但自从step 为 1,input 中的值四舍五入为 3。最后,step 设置为 0.1 – 太晚了。

如何确保 step 属性的值在 ngModel 设置输入的 value 之前绑定

在上面的示例中,滑块在页面加载时位于页面上。在我的真实代码中,动态添加了多个新滑块。每当添加它们时,它们都应该以正确的顺序绑定它们的 stepvalue

我不喜欢的解决方法

解决方法是在模板中对 step 进行硬编码,而不是动态设置它。如果我这样做,我的自定义滑块指令将没有用,我会删除它并直接使用 <input>

<input type="range" min="2" max="3" step="0.1" ng-model="someNumber">

如果我使用它,则滑块的 value 设置正确,没有四舍五入。但我想保留我的自定义指令 customSlider,以便抽象 step 的计算。

我尝试过但没有用的东西

  • 更改模板中 stepng-model 属性的顺序没有任何效果。
  • 我开始尝试添加一个 compile 函数,而不仅仅是一个 link 函数。但是我无法在compile中设置stepSize,因为scope在编译阶段不可用。
  • 我尝试将指令的 link 函数拆分为前 link 和 post-link 函数。但是无论我在 pre 还是 post 中设置 scope.stepSize,页面都像以前一样工作。
  • 在设置 scope.stepSize 后立即手动调用 scope.$digest() 只会抛出 Error: [$rootScope:inprog] $digest already in progress.
  • 我认为自定义指令优先级不适合这个,因为只涉及一个自定义指令。我对 step 值的绑定不是自定义指令,它只是原始 {{}} 绑定。而且我认为绑定 step 是一项足够简单的任务,不应将其包装在自己的指令中。

我不确定这是否是正确的方法,但是向主控制器添加超时可以解决问题。

$timeout(function() {
    $scope.someNumber = 2.5;
});

编辑:虽然一开始看起来像是一个肮脏的 hack,但请注意,(最终)$scope 变量的分配晚于模板是很常见的,因为需要额外的 ajax 调用来检索值.

在您的 link 函数中,通过添加 step 属性来操作 DOM。

您还可以通过将 theNum: '=ngModel' 放入 scope 来简化与外部 ngModel 的绑定。

var app = angular.module('HelloApp', []);

app.directive('customSlider', function () {
    var tpl = "2 <input type='range' min='2' max='3' ng-model='theNum' /> 3";
    
    return {
        restrict: 'E',
        template: tpl,
        require: 'ngModel',
        scope: {
            fractionSize: '=',
            theNum: '=ngModel'
        },
        link: function (scope, element, attrs, ngModelCtrl) {
            var e = element.find('input')[0];
            
            var step = 1 / scope.fractionSize;
            e.setAttribute('step', step);
            
            scope.$watch('fractionSize', function (newVal) {
                if (newVal) {
                    var step = 1 / newVal;
                    e.setAttribute('step', step);
                }
            });
        }
    };
});

angular.module('HelloApp').controller('HelloController', function($scope) {
    $scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>

<div ng-app="HelloApp" ng-controller="HelloController">
    <h3>Custom slider</h3>
    
    <custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
    
    <h3>View/edit the slider’s value</h3>
    <input ng-model="someNumber"></input> {{ someNumber }}
</div>

进一步 直接 DOM 操作,我根本不会在内部输入上有另一个 ng-model,而是总是 set/get 上的属性直接输入

  • 当 setting/getting 值来自输入时,您需要考虑一个抽象级别。原始 DOM 事件和元素。

  • 因此,您不受 Angular 允许的此类元素的限制(事实上:如果没有解决方法,它所做的似乎无法处理您的用例)。如果浏览器允许范围输入,你可以这样做。

  • 有 2 个 ngModelControllers 在一个 UI 设置一个值的小部件上运行可能会有点混乱,至少对我来说是这样!

  • 如果您 need/want 使用它,您仍然可以访问外部 ngModelController 管道及其所有关于解析器和验证器的功能。

  • 您可以节省额外的观察者(但这可能是 micro/premature 优化)。

请参阅下面的示例。

angular.module('HelloApp', []);

angular.module('HelloApp').directive('customSlider', function() {
    var tpl = "2 <input type='range' min='2' max='3' /> 3";
    
    return {
        restrict: 'E',
        template: tpl,
        require: 'ngModel',
        scope: {
            fractionSize: '='
        },
        link: function(scope, element, attrs, ngModelCtrl) {
            var input = element.find('input');          
            input.prop('step', 1 / scope.fractionSize);

            input.on('input', function() {
              scope.$apply(function() {
                ngModelCtrl.$setViewValue(input.prop('value'));
              });
            });

            ngModelCtrl.$render = function(value) {
               input.prop('value', ngModelCtrl.$viewValue);
            };
        }
    };
});

angular.module('HelloApp').controller('HelloController', function($scope) {
    $scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>

<div ng-app="HelloApp" ng-controller="HelloController">
    <h3>Custom slider</h3>
    
    <custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
    
    <h3>View/edit the slider’s value</h3>
    <input ng-model="someNumber"></input>
</div>

也可在 http://plnkr.co/edit/pMtmNSy6MVuXV5DbE1HI?p=preview