2013-03-07 6 views
28

Sto scrivendo una direttiva che richiede un ambito isolato, ma desidero associarlo all'ambito padre tramite ngModel.

Qui il problema è che il valore dell'ambito del genitore non viene modificato.

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.

Questo funziona bene se io non uso un ambito isolato per la direttiva

Demo: Fiddle.

+0

caso non 'element.html (ngModel $ viewValue ....) Essere' element.html ($ sce.getTrustedHtml (ngModel $. viewValue) ..) 'So che questo è quasi esattamente lo stesso dell'esempio di ng docs, ma ho appena scoperto che in questo modo ignora la protezione di xss altrimenti. – cirrus

+0

@arun Puoi spiegare perché l'ambito isolato lo rende non funzionante? – geckob

+0

Re @cirrus comment, mi sembra che per evitare XSS, ogni volta che qualcosa viene inserito nell'elemento dovrebbe prima essere disinfettato con $ sanitize (cioè non fidarsi dell'HTML). Qualcosa come element.html ($ sanitize (ngModel. $ ViewValue)) '. – Soferio

risposta

41

Il motivo è che poiché si sta creando un ambito isolato per la propria direttiva contenteditable, la direttiva ng-model sullo stesso elemento ottiene anche tale ambito isolato. Ciò significa che hai due diversi ambiti che non sono collegati tra loro, che hanno entrambi una proprietà form.userContent che cambia separatamente. Credo che si potrebbe esemplificare che da questo codice:

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

Come vedrete nella vostra console, ci sono due ambiti diversi e form.userContent su di loro cambiare separatamente, se si modifica il testo nella textarea o se si modifica la testo nel tuo divento contenteditable

Quindi scommetto che stai pensando "abbastanza con la spiegazione e mostrami una soluzione!". Bene, non c'è (a mia conoscenza) una soluzione carina per questo, ma ce n'è uno che funziona. Quello che vuoi fare è portare un riferimento del modello nel tuo ambito isolato e assicurarti che abbia lo stesso nome nel tuo ambito isolato come nell'ambito genitore.

Ecco quello che fai, invece di creare un ambito vuoto come questo:

... 
scope: {} 
... 

si associa il modello come questo:

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

Ora avete una proprietà model sulla vostra portata isolata che è un riferimento a form.userContent sull'ambito genitore. Ma ng-model non è alla ricerca di una proprietà model, sta cercando un form.userProperty che ancora non esiste nel nostro ambito isolato. Quindi, per risolvere questo problema, aggiungiamo questo all'interno della nostra funzione di collegamento:

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

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

Il primo orologio sincronizza i cambiamenti su form.userContent che viene da fuori della nostra direttiva al nostro isolato form.userContent, e il secondo orologio fa in modo che ci propaghiamo eventuali modifiche sul nostro form.userContent isolato fino all'ambito principale.

Mi rendo conto che questa è una risposta lunga e forse non molto facile da seguire. Quindi sarei felice di chiarire qualsiasi cosa tu ritenga sia sfocata.

+2

Quando si dice "[...] la direttiva ng-model sullo stesso elemento ottiene anche quell'ambito isolato, il che significa che si hanno due diversi ambiti che non sono collegati tra loro", se lo stesso ambito isolato è condiviso da 'ng-model' e' contenteditable', come mai allora dici di avere due diversi ambiti? Sembra una contraddizione. O mi sta sfuggendo qualcosa? – Behrang

+0

@anders Non sono chiaro in che modo lo scope isolato infrange il codice. Puoi chiarire per favore? – geckob

+0

@ Anders Ekdahl Se più direttive su un elemento forniscono un ambito isolato, viene applicato solo un nuovo ambito. (Riferimento Ng-Book, pagina 111, Titolo dell'opzione Ambito) –

4

la prima risposta spiega bene il problema, credo di avere una soluzione più semplice che evita gli orologi extra.

per riassumere la risposta 1.ngModel non può funzionare all'interno dell'ambito isolato perché gli elementi a cui si intendeva associare non sono nel suo ambito. sono nello scope genitore.

soluzione 1, si legano alla proprietà del genitore

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

diventa

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

soluzione 2, spostare ngModel al di fuori del campo di applicazione isolare

require : '?ngModel', diventa require : '?^ngModel', l'^ dice tua direttiva a guardare negli elementi parent per ngModel

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

diventa

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

Ho trovato questa risposta essere al punto e più utile. Potresti voler completare la risposta con l'approccio dual-watch perché è una soluzione legittima (e ampiamente applicata). –

-1

Ido't sanno quello che u esattamente vuole, u può provare questo.

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