2013-03-07 13 views
28

Piszę dyrektywę, która potrzebuje izolowany zakres, ale chcę powiązać go do zakresu nadrzędnego przez ngModel.

Problem polega na tym, że wartość zasięgu rodzica nie ulega zmianie.

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

Demo: Fiddle.

Działa to dobrze, jeśli nie używać samodzielnie do zakresu dyrektywy

Demo: Fiddle.

+0

Gdyby nie 'element.html (ngModel $ viewValue ....) Będzie' element.html ($ sce.getTrustedHtml (ngModel $. viewValue) ..) "Wiem, że to prawie dokładnie to samo, co w przykładzie na Ng docs, ale właśnie odkryłem, że w ten sposób skutecznie pomija ochronę xss. – cirrus

+0

@arun Czy możesz wyjaśnić, dlaczego zakres izolatu sprawia, że ​​nie działa? – geckob

+0

Komentarz @rirrus, wydaje mi się, że aby uniknąć XSS, za każdym razem, gdy cokolwiek zostanie umieszczone w elemencie, powinno najpierw zostać odkażone przez $ sanitize (tj. Nie ufaj HTML). Coś jak element.html ($ sanitize (ngModel. $ ViewValue)) '. – Soferio

Odpowiedz

41

Powodem jest to, że ponieważ tworzysz izolowany zakres dla dyrektywy contenteditable, dyrektywa ng-model dla tego samego elementu również otrzyma ten izolowany zakres. Oznacza to, że masz dwa różne zakresy, które nie są ze sobą połączone, a oba mają właściwość form.userContent, która zmienia się osobno. Chyba można zilustrować go przez ten kod:

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

Jak zobaczysz w konsoli, istnieją dwa różne zakresy i form.userContent na ich zmieniać oddzielnie, jeśli zmienić tekst w textarea lub w przypadku zmiany tekst w twoim contenteditable div.

Założę się, że myślisz "wystarczy z wyjaśnieniem i pokaż mi rozwiązanie!". Cóż, nie ma (według mojej wiedzy) całkiem odpowiedniego rozwiązania, ale jest jedna, która działa. To, co chcesz zrobić, to wprowadzić odniesienie modelu do wyizolowanego zakresu i upewnić się, że ma ono taką samą nazwę w odizolowanym zakresie, jak w zakresie nadrzędnym.

Oto co trzeba zrobić, zamiast tworzyć pustą zakres takiego:

... 
scope: {} 
... 

powiązać model takiego:

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

Teraz masz nieruchomość model na pojedyncze zakresie, jest odniesieniem do form.userContent w twoim zasięgu nadrzędnym. Ale ng-model nie szuka właściwości model, szuka ona form.userProperty, która wciąż nie istnieje w naszym wyizolowanym zakresie. Tak aby rozwiązać ten problem, dodajemy to wewnątrz naszej funkcji łączącej:

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

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

Pierwszy zegarek synchronizuje zmiany na form.userContent że pochodzi spoza naszego dyrektywy do naszego izolowanym form.userContent, a drugi zegarek daje pewność, że mamy propagować żadnych zmian na naszym izolowanym form.userContent aż do zakresu nadrzędnego.

Zdaję sobie sprawę, że jest to długa odpowiedź i być może nie bardzo łatwa do naśladowania. Tak więc z radością wyjaśniłbym wszystko, co wydaje ci się niewyraźne.

+2

Kiedy mówisz "[...] dyrektywa ng-model na tym samym elemencie dostaje również ten izolowany zakres, co oznacza, że ​​masz dwa różne zakresy, które nie są ze sobą połączone", jeśli ten sam izolowany zakres jest współdzielone przez "ng-model" i "contenteditable", jak to się stało, że mówisz, że mają dwa różne zakresy? Wygląda na sprzeczność. Czy może czegoś brakuje? – Behrang

+0

@andres Nie jestem pewien, w jaki sposób zakres izolatu przerywa kod. Czy możesz wyjaśnić? – geckob

+0

@ Anders Ekdahl Jeśli wiele dyrektyw dotyczących elementu zapewni zakres izolowania, zostanie zastosowany tylko jeden nowy zakres (odniesienie do książki Ng, strona 111, nagłówek opcji zakresu) –

4

Pierwsza odpowiedź wyjaśnia dobrze problem, uważam, że mam prostsze rozwiązanie, które pozwala uniknąć dodatkowych zegarków.

Podsumowując odpowiedź 1.ngModel nie może działać wewnątrz zakresu izolowania, ponieważ elementy, do których ma się on wiązać, nie wchodzą w jego zakres. są w zakresie nadrzędnym.

rozwiązanie 1, wiążą się z majątku rodziców

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

staje

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

rozwiązanie 2, przesuń ngModel poza izolowania zakresu

require : '?ngModel', staje require : '?^ngModel',^opowiada swoją dyrektywę wyglądać w elementach rodzica dla ngModel

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

staje

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

Ta odpowiedź okazała się bardzo przydatna. Możesz udzielić odpowiedzi z podejściem dual-$ watch, ponieważ jest to uzasadnione (i szeroko stosowane) rozwiązanie. –

-1

Ido't wiedzieć dokładnie, co u chcą, u można spróbować.

html:

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

js

angular.module('customControl', []).directive('contenteditable', function() { 
    return { 
     restrict : 'A', // only activate on element attribute 
     require : '?ngModel', // get a hold of NgModelController 
     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({'content': element.html()}); 
      } 
     } 
    }; 
}); 
Powiązane problemy