web-dev-qa-db-ja.com

ngModel。$ setViewValue(...)が機能しないのはなぜですか

孤立したスコープを必要とするディレクティブを書いていますが、 ngModel を介して親スコープにバインドしたいです。

ここでの問題は、親のスコープ値が変更されていないことです。

Markup

<form name="myForm" ng-app="customControl">
    <div ng-init="form.userContent"></div>
    <div contenteditable name="myWidget" ng-model="form.userContent" required>Change me!</div>
    <span ng-show="myForm.myWidget.$error.required">Required!</span>
    <hr />
    <textarea ng-model="form.userContent"></textarea>
</form>

JS

angular.module('customControl', []).directive('contenteditable', function() {
    return {
        restrict : 'A', // only activate on element attribute
        require : '?ngModel', // get a hold of NgModelController
        scope: {},
        link : function(scope, element, attrs, ngModel) {
            if (!ngModel)
                return; // do nothing if no ng-model

            // Specify how UI should be updated
            ngModel.$render = function() {
                element.html(ngModel.$viewValue || '');
            };

            // Listen for change events to enable binding
            element.bind('blur keyup change', function() {
                        scope.$apply(read);
                    });
            read(); // initialize

            // Write data to the model
            function read() {
                ngModel.$setViewValue(element.html());
            }
        }
    };
});

デモ: Fiddle

ディレクティブに孤立したスコープを使用しない場合、これはうまく機能します

デモ: Fiddle

29
Arun P Johny

理由は、contenteditableディレクティブの分離スコープを作成しているため、同じ要素のng-modelディレクティブもその分離スコープを取得するためです。つまり、相互に接続されていない2つの異なるスコープがあり、どちらも別々に変更されるform.userContentプロパティを持っています。このコードでそれを例示できると思います:

<!doctype html>
<html ng-app="myApp">
<head>
    <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
    <script src="http://code.angularjs.org/1.0.5/angular.min.js"></script>
    <script>
    angular.module('myApp', []).controller('Ctrl', function($scope) {

    })
    .directive('contenteditable', function() {
        return {
            restrict : 'A', // only activate on element attribute
            require : '?ngModel', // get a hold of NgModelController
            scope: {},
            link : function(scope, element, attrs, ngModel) {
                if (!ngModel)
                    return; // do nothing if no ng-model

                setInterval(function() {
                    if (angular.element('#contenteditable').scope().form)
                        console.log(angular.element('#contenteditable').scope().form.userContent);

                    if (angular.element('#textarea').scope().form)
                        console.log(angular.element('#textarea').scope().form.userContent);
                }, 1000);

                // Specify how UI should be updated
                ngModel.$render = function() {
                    element.html(ngModel.$viewValue || '');
                };

                // Listen for change events to enable binding
                element.bind('blur keyup change', function() {
                            scope.$apply(read);
                        });
                read(); // initialize

                // Write data to the model
                function read() {
                    ngModel.$setViewValue(element.html());
                }
            }
        };
    });
    </script>
</head>
<body ng-controller="Ctrl">
    <form name="myForm">
        <div ng-init="form.userContent"></div>
        <div contenteditable name="myWidget" ng-model="form.userContent" id="contenteditable" required>Change me!</div>
        <span ng-show="myForm.myWidget.$error.required">Required!</span>
        <hr />
        <textarea ng-model="form.userContent" id="textarea"></textarea>
    </form>
</body>
</html>

コンソールに表示されるように、2つの異なるスコープがあり、textareaのテキストを変更した場合、またはcontenteditable divのテキストを変更した場合、form.userContentは別々に変更されます。

だから、あなたは「説明で十分で、解決策を見せて!」と考えているに違いない。まあ、これには(私の知る限りでは)きれいな解決策はありませんが、うまくいくものがあります。あなたがしたいことは、モデルの参照を隔離されたスコープに持ち込み、隔離されたスコープに親スコープと同じ名前があることを確認することです。

このような空のスコープを作成する代わりに、次のようにします。

...
scope: {}
...

次のようにモデルをバインドします。

...
scope: {
    model: '=ngModel'
}
....

これで、親スコープのform.userContentへの参照である分離スコープにmodelプロパティができました。しかし、ng-modelmodelプロパティを探しているのではなく、分離スコープにまだ存在しないform.userPropertyを探しています。これを修正するために、リンク関数内にこれを追加します:

scope.$watch('model', function() {
    scope.$eval(attrs.ngModel + ' = model');
});

scope.$watch(attrs.ngModel, function(val) {
    scope.model = val;
});

最初のウォッチは、ディレクティブの外部からのform.userContentの変更を分離されたform.userContentに同期し、2番目のウォッチは、分離されたform.userContentの変更を親に確実に伝播します範囲。

これは長い答えであり、たぶん従うのは簡単ではないことを理解しています。だから、私はあなたがぼやけていると感じるものは何でもクリアしたいと思います。

43
Anders Ekdahl

最初の答えは問題をうまく説明しています、余分な時計を避ける簡単な解決策があると思います。

答えをまとめると1. ngModelは、それをバインドしようとした要素がスコープ内にないため、分離スコープ内では機能しません。それらは親スコープ内にあります。

ソリューション1、親のプロパティにバインド

<div contenteditable name="myWidget" ng-model="form.userContent" required>Change me!</div>

になる

<div contenteditable name="myWidget" ng-model="$parent.form.userContent" required>Change me!</div>

解決策2、ngModelを分離スコープの外に移動する

require : '?ngModel',require : '?^ngModel',になります^は、ngModelの親要素を調べるようにディレクティブに指示します

<div contenteditable name="myWidget" ng-model="form.userContent" required>Change me!</div>

になる

<div ng-model="form.userContent">
    <div contenteditable name="myWidget" required>Change me!</div>
</div>
4
FreakinaBox