web-dev-qa-db-fra.com

Pourquoi ngModel. $ SetViewValue (...) ne fonctionne pas depuis

J'écris une directive qui a besoin d'une portée isolée, mais je veux la lier à la portée parent via ngModel .

Ici, le problème est que la valeur de portée du parent n'est pas modifiée.

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());
            }
        }
    };
});

Démo: Fiddle .

Cela fonctionne bien si je n'utilise pas un champ d'application isolé pour la directive

Démo: Fiddle .

29
Arun P Johny

La raison en est que puisque vous créez une étendue isolée pour votre directive contenteditable, la directive ng-model Sur le même élément obtient également cette étendue isolée. Ce qui signifie que vous avez deux étendues différentes qui ne sont pas connectées l'une à l'autre, qui ont toutes deux une propriété form.userContent Qui change séparément. Je suppose que vous pouvez l'illustrer par ce code:

<!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>

Comme vous le verrez dans votre console, il existe deux étendues différentes et form.userContent Sur elles changent séparément si vous changez le texte dans la zone de texte ou si vous changez le texte dans votre div contentable.

Je parie donc que vous pensez "assez avec l'explication et montrez-moi une solution!". Eh bien, il n'y a pas (à ma connaissance) une jolie solution pour cela, mais il y en a une qui fonctionne. Ce que vous voulez faire, c'est introduire une référence du modèle dans votre portée isolée et vous assurer qu'il porte le même nom dans votre portée isolée que dans la portée parent.

Voici ce que vous faites, au lieu de créer une portée vide comme celle-ci:

...
scope: {}
...

Vous liez le modèle comme ceci:

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

Vous avez maintenant une propriété model sur votre portée isolée qui est une référence à form.userContent Sur votre portée parent. Mais ng-model Ne cherche pas une propriété model, il cherche un form.userProperty Qui n'existe toujours pas dans notre portée isolée. Donc, pour résoudre ce problème, nous ajoutons cela dans notre fonction de liaison:

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

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

La première montre synchronise les changements sur form.userContent Qui viennent de l'extérieur de notre directive vers notre form.userContent Isolé, et la deuxième montre s'assure que nous propageons tous les changements sur notre form.userContent Isolé à la portée parent.

Je me rends compte que c'est une longue réponse, et peut-être pas très facile à suivre. Je serais donc heureux de clarifier tout ce qui vous semble flou.

43
Anders Ekdahl

la première réponse explique bien le problème, je crois avoir une solution plus simple qui évite les montres supplémentaires.

pour résumer la réponse 1. ngModel ne peut pas fonctionner à l'intérieur de la portée d'isolement car les éléments auxquels vous vouliez le lier ne sont pas dans sa portée. ils sont dans la portée parent.

solution 1, liez à la propriété du parent

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

devient

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

solution 2, déplacez ngModel hors de la portée de l'isolat

require : '?ngModel', devient require : '?^ngModel', le ^ indique à votre directive de chercher dans les éléments parents pour ngModel

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

devient

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