【问题标题】:In Angular, bind attribute from scope variable *before* ngModel binds the input’s `value`在 Angular 中,来自范围变量 *before* ngModel 的绑定属性绑定输入的 `value`
【发布时间】:2016-01-22 11:23:44
【问题描述】:

在我的 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。

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

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

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

我不喜欢的解决方法

一种解决方法是在模板中硬编码step,而不是动态设置它。如果我这样做,我的自定义滑块指令将毫无用处,我将删除它并直接使用 &lt;input&gt;

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

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

我尝试过的方法不起作用

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

【问题讨论】:

标签: angularjs data-binding angular-ngmodel


【解决方案1】:

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

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

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

【讨论】:

  • 恐怕这对我不起作用。在我的真实应用程序中,滑块通过ngRepeat 指令添加到页面中,以响应用户发起的将数字附加到数组中。所以我的应用程序相当于$scope.someNumber 总是在创建编辑它的滑块之前存在。而且我的应用程序完全离线,所以超时是不合适的。
  • 看起来它被提升为角度的issue。您可能想参考那里的临时解决方案。
【解决方案2】:

在您的 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>

【讨论】:

    【解决方案3】:

    进一步采用@pixelbits' answer 的直接 DOM 操作,我根本不会在内部输入上再有另一个ng-model,而是始终直接设置/获取输入上的属性

    • 在设置/获取输入值时,您需要考虑一个抽象级别。原始 DOM 事件和元素。

    • 因此,您不仅限于 Angular 在此类元素上允许的内容(实际上:如果没有变通方法,它似乎无法处理您的用例)。如果浏览器允许在范围输入上使用,您可以这样做。

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

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

    • 您可以省去额外的观察者(但这可能是微/过早的优化)。

    请参阅下面的示例。

    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获得

    【讨论】:

    • scope.$apply 似乎是不必要的——即使没有scope.$applyinput 处理程序仍然可以工作。我认为这是因为ngModelCtrl.$setViewValue 在运行时会触发$digest。在 DOM 操作中“以防万一”使用scope.$apply 是否是一种好习惯,即使当前代码没有必要这样做?
    • @RoryO'Kane 我认为setViewValue 不会触发摘要,因为a)在没有它的情况下进行测试时,在滑动范围后模型更改在“查看/编辑”部分中不可见; b)如果它无条件触发它,在我的示例中会出现控制台错误,因为它已经在运行时触发它,并且没有
    • 我以为$setViewValue 开始了一个摘要,因为its docs 说“只有在updateOn 事件被触发时才会触发$digest……”,但也许这意味着它不会@如果没有设置 updateOn 属性,则根本不存在 987654337@。尽管如此,当我编辑您的示例以删除周围的 scope.$apply 时,当我拖动滑块时模型更改是可见的。也许这是一个特定于浏览器的东西——我正在使用 Chrome。为了安全起见,我将在代码中使用$apply
    • @RoryO'Kane 啊,你说得对,不需要它。我不确定我第一次测试它时在做什么......
    猜你喜欢
    • 2020-09-30
    • 2017-08-16
    • 1970-01-01
    • 2018-11-29
    • 1970-01-01
    • 2021-03-30
    • 1970-01-01
    • 2021-11-15
    • 2018-03-20
    相关资源
    最近更新 更多