自定义指令的多个实例,与 ngModel 混淆

Multiple instances of custom directive, confusion with ngModel

我创建了一个自定义指令来呈现问题的滑块(本质上是包装 jquery ui 滑块)。该指令采用一个 ngModel 并在用户使用滑块时更新它,并且有一个 $watch 附加到父模型(传递给指令的 ngModel 只是父模型的一部分)。该指令在一个页面上有多个实例。

我遇到了手表问题,手表似乎总是出现在页面上的最后一个问题上。因此,例如一个包含 10 个问题的页面,在问题 1 上使用滑块 - 在最后一个问题(问题 10)上触发手表。我认为这个问题与 directives/isolated scope and/or watch 函数有关,但我无法解决它。

this.app.directive('questionslider', () => {
    var onChangeEvent = (event, ui) => {
        updateModel(ui.value);
    };

    var onSlideEvent = (event, ui) => {
        updateUi(event, ui);
    };

    var updateUi = (event, ui) => {
        $(ui.handle).find(".ff-handle-glyph > div").css("top", (ui.value) * - 10);
    }

    var updateModel = (newValue) => {
        // find value in values list...
        angular.forEach(isolatedScope.model.PossibleValues, function(value) {
            if (parseInt(value.Name) === newValue) {
                isolatedScope.$apply(function() {
                    isolatedScope.model.Value = value;
                });
            }
        });
    };

    var isolatedScope: any;

    return {
        restrict: 'AE',
        replace: true,
        template: '<div></div>',
        scope: {
            model: '=ngModel',
        },
        link: function(scope, element, attrs, ctrl) {
            isolatedScope = scope;
            scope.$watch(ngModelCtrl, function() {
                // use provided defaultValue if model is empty
                var value = isolatedScope.model.Value === null ? isolatedScope.model.DefaultValue : isolatedScope.model.Value;

                element.slider({
                    min: 0,
                    max: isolatedScope.model.PossibleValues.length,
                    value: value.Name,
                    change: onChangeEvent,
                    slide: onSlideEvent
                });
            }
        }
    };
};

在控制器中添加手表的代码

this.$scope.questions.forEach(function(question) {
    this.$scope.$watch(
        function() { return question; },
        function(newVal, oldVal) { this.updateQuestion(newVal, oldVal) },
        true
    );
});

UpdateQuestion 函数(现在只输出当前问题)

function updateQuestion(newVal, oldVal) {
    // prevent event on initial load
    if (newVal === oldVal) {
        return;
    }

    console.log(newVal);
}

ng-repeat 标记实例化问题滑块

<div data-ng-repeat="question in Questions">
    <h4>{{question.QuestionText}}</h4>
    <p>{{question.RangeMinText}}</p>
    <questionslider ng-model="question"></questionslider>        
    <p>{{question.RangeMaxText}}</p>
</div>

问题 JSON 看起来像这样

{
    "DefaultValue": {
        "Id": "5",
        "Name": "5"
    },
    "Id": "1",
    "IsAnswered": false,
    "PossibleValues": [
        {
            "Id": "1",
            "Name": "1"
        },
        {
            "Id": "2",
            "Name": "2"
        },
        {
            "Id": "3",
            "Name": "3"
        },
        {
            "Id": "4",
            "Name": "4"
        },
        {
            "Id": "5",
            "Name": "5"
        },
        {
            "Id": "6",
            "Name": "6"
        },
        {
            "Id": "7",
            "Name": "7"
        },
        {
            "Id": "8",
            "Name": "8"
        },
        {
            "Id": "9",
            "Name": "9"
        },
        {
            "Id": "10",
            "Name": "10"
        }
    ],
    "QuestionText": "hows it haning?",
    "RangeMaxText": "not good",
    "RangeMinText": "Very good",
    "Type": 0,
    "Value": null
}
],
"Title": "Question title",
"Type": 0
}

所以问题是,无论我使用滑块指令更新哪个问题,它始终是传递给 updateQuestion 的页面上的最后一个问题。

更新 我尝试使用 $watchCollection,但似乎没有任何东西可以触发事件。

this.$scope.$watchCollection(
    'questions',
    function (newVal, oldVal) {
        // prevent event on initial load
        if (newVal === oldVal) {
            return;
        }

        for (var i = 0; i < newVal.length; i++) {
            if (newVal[i] != oldVal[i]) {
                this.$log.info("update : " + newVal.Id);
            }
        }
    }
);

我也试过

function() { return questions; }

作为第一个表达式。仍然没有运气。

也许对每个问题使用单独的控制器是我唯一的选择,但这似乎是一种解决方法。

更新 所以我尝试为每个问题使用单独的控制器,在控制器中为每个问题添加一个手表,奇怪的是即使这样也会重现相同的场景。依然是页面上最后一题传入watch函数

标记

<div data-ng-repeat="question in Questions" data-ng-controller="QuestionInstanceController">
    <h4>{{question.QuestionText}}</h4>
    <p>{{question.RangeMinText}}</p>
    <questionslider ng-model="question"></questionslider>        
    <p>{{question.RangeMaxText}}</p>
</div>

控制器代码

app.controller('QuestionInstanceController', function($scope) {
        console.log($scope.question); // outputs correct question here!
        $scope.$watch(
            function() { return $scope.question; },
            function(newValue, oldValue) { 
                console.log(newValue); // always outputs last question on page
            },
            true);
    }
}

这一定与我的自定义指令有关,当页面上有多个实例时,我是否以某种方式覆盖了以前的实例?

我预计您的 $scope.$watch 逻辑会让您失望。最简单的方法可能是使用 $watchCollection:

监视整个问题数组
$scope.$watchCollection(questions, function(newValue, oldValue) {
    for(index=0; index<newValue.length; index++) {
        if (newValue[index] !== oldValue[index]) {
            console.log("Question " + index + " updated to: " + newValue[index]);
        }
    }
});

否则,您可能可以为 ng-repeat 循环中的每个项目创建一个单独的控制器,并在那里设置一个处理更改的手表。我也不喜欢这个解决方案,因为它有点冗长。首先,一个新的控制器来处理问题:

app.controller('QuestionCtrl', function($scope) {
    $scope.$watch('question', function(newValue, oldValue) {
            if (newVal === oldVal) {
                return;
            }

            console.log(newVal);
    }
});

您的视图略有修改以包含对新 QuestionCtrl 的引用:

<div data-ng-repeat="question in Questions" ng-controller='QuestionCtrl'>
    <h4>{{question.QuestionText}}</h4>
    <p>{{question.RangeMinText}}</p>
    <questionslider ng-model="question"></questionslider>        
    <p>{{question.RangeMaxText}}</p>
</div>

This article 提供了有关将控制器与 ng-repeat 结合使用的更多信息。

我希望其中之一能帮助你。

所以我设法自己找到了解决方案。问题是我在 link() 的每个 运行 上分配的指令中有一个 "var isolatedScope;"。我认为指令中声明的变量在每个实例上都是独立的,但实际上它们在指令的每个实现中都被覆盖了。

因此解决方案是将 onChange 的实现直接移动到滑块组件的 init,从而避免以后访问范围变量的需要

this.app.directive('questionslider', () => {
    var onChangeEvent = (event, ui) => {

    };

    var onSlideEvent = (event, ui) => {
        updateUi(ui.value, ui.handle);
    };

    var updateUi = (value, element) => {

    }

    var onStartSlide = (event, ui) => {

    }

    var onEndSlide = (event, ui) => {

    }

    return {
        restrict: 'AE',
        require: 'ngModel',
        replace: true,
        templateUrl: 'templates/questionSlider.html',
        scope: {
            model: '=ngModel'
        },
        link: (scope: any, element, attrs, ngModelCtrl: ng.INgModelController) => {
            scope.$watch(ngModelCtrl, () => {
                // use provided defaultValue if model is empty
                var value = scope.model.Value === null ? scope.model.DefaultValue : scope.model.Value;
                var hasNoValue = scope.model.Value === null;

                if (hasNoValue) {
                    element.find('.ui-slider-handle').addClass('dg-notset');
                }

                element.slider({
                    min: parseInt(scope.model.PossibleValues[0].Name),
                    max: scope.model.PossibleValues.length,
                    value: parseInt(value.Name),
                    slide: onSlideEvent,
                    start: onStartSlide,
                    stop: onEndSlide,
                    animate: true,
                    change: (event, ui) => {
                        // find value in values list...
                        angular.forEach(scope.model.PossibleValues, (val) => {
                            if (parseInt(val.Name) === ui.value) {
                                scope.$apply(() => {
                                    scope.model.Value = val;
                                });
                            }
                        });

                        onChangeEvent(event, ui);
                    }
                });
            });
        }
    };
});