Credo che il "Non manipolare il DOM dai controller" mantra è tornato dai giorni, quando le direttive principalmente/utilizzati solo funzioni di collegamento (o controllori di direttive in cui solo un modo per comunicare tra loro con altre direttive).
La best practice attualmente suggerita è quella di utilizzare "componenti" (che possono essere realizzati tramite direttive), in cui praticamente tutta la logica direttiva parte nel controller. (Nota ad esempio che in Angular 2 non ci sono funzioni di collegamento e ogni componente/direttiva è fondamentalmente una classe/controller (più alcuni metadati).)
In questo contesto, ritengo che sia perfettamente corretto manipolare il DOM in a della direttiva della direttiva dal della direttiva.
L'idea è di mantenere i vostri modelli/HTML dichiarativi. Confrontare i seguenti frammenti:
<!--
`SomeController` reaches out in the DOM and
makes changes to `myComponent`'s template --- BAD
-->
<div ng-controller="SomeController">
...
<my-component></my-component>
...
</div>
vs
<div ng-controller="SomeController">
...
<!--
`myComponent`'s controller makes changes to
`myComponent`'s template --- OK
-->
<my-component></my-component>
...
</div>
Nel primo esempio (cattivo), myComponent
avranno diverso comportamento/aspetto a seconda di dove nel DOM sembra (ad esempio è sotto SomeController
?). Ciò che è più importante, è molto difficile scoprire quale altra parte (non correlata) potrebbe cambiare il comportamento/aspetto di myComponent
.
Nel secondo (buon) esempio, il comportamento e l'aspetto di myComponent
saranno coerenti in tutta l'app ed è molto facile scoprire cosa sarà: devo solo cercare nella definizione della direttiva (un posto) .
Ci sono un paio di avvertimenti però:
Non si vuole mescolare il codice di manipolazione del DOM con altra logica. (Renderebbe il tuo codice meno manutenibile e più difficile da testare).
Spesso, si desidera manipolare il DOM nella fase di post-collegamento, quando tutti i bambini sono a posto (compilato + collegato).L'esecuzione del codice di manipolazione DOM durante l'istanziazione del controller significherebbe che il contenuto del modello non è ancora stato elaborato.
In genere, non si desidera eseguire la manipolazione DOM quando il controller non viene istanziato nel contesto di una direttiva, poiché ciò significherebbe che è sempre necessario un modello compilato per testare il controller. Ciò non è desiderabile, perché rende più lenti i test delle unità, anche se si desidera testare solo altre parti della logica del controller che non sono correlate a DOM/HTML.
Quindi, cosa possiamo fare?
Isolare il codice di manipolazione DOM in una funzione dedicata. Questa funzione verrà chiamata quando appropriato (vedi sotto), ma tutte le interazioni DOM saranno in un unico posto, il che rende più facile la revisione.
Esporre questa funzione come metodo di controllo e chiamarla dalla funzione di collegamento della direttiva (anziché durante l'inizializzazione del controller). Ciò garantisce che il DOM si trovi nello stato desiderato (se necessario) e disaccoppia anche l'istanza del controller "stand-alone" dalla manipolazione del DOM.
Cosa ci guadagno:
Se il controller viene istanziato come parte della direttiva compilazione/linking, il metodo sarà chiamato e il DOM verrà manipolato, come previsto.
Nei test di unità, se non è necessaria la logica di manipolazione DOM, è possibile creare un'istanza diretta del controllore e testarne la logica di business (indipendentemente da qualsiasi DOM o compilazione).
Hai più controllo su quando avviene la manipolazione del DOM (in unit test). Per esempio. puoi istanziare direttamente il controller, ma comunque passare in un $element
, fare tutte le asserzioni che potresti voler fare, quindi chiamare manualmente il metodo di manipolazione del DOM e asserire che l'elemento è stato trasformato correttamente. È anche più facile passare in un $element
deriso e cose come aggiungere listener di eventi, senza dover configurare un vero DOM.
Lo svantaggio di questo approccio (esponendo il metodo e chiamandolo dalla funzione di collegamento), è la piastra supplementare. Se si utilizza Angular 1.5.x, è possibile risparmiare il boilerplate utilizzando i ganci del ciclo di vita del controllore direttivo (ad esempio $onInit
o $postLink
), senza la necessità di disporre di una funzione di collegamento, solo per ottenere il controller e richiamare un metodo su di esso . (caratteristica bonus: Utilizzando la sintassi componente 1.5.x con ganci ciclo di vita, faciliterebbe a migrare verso angolari 2.)
Esempi:
Prima v1.5.x
.directive('myButton', function myButtonDirective() {
// DDO
return {
template: '<button ng-click="$ctrl.onClick()></button>',
scope: {}
bindToController: {
label: '@'
}
controllerAs: '$ctrl',
controller: function MyButtonController($element) {
// Variables - Private
var self = this;
// Functions - Public
self._setupElement = _setupElement;
self.onClick = onClick;
// Functions - Definitions
function _setupElement() {
$element.text(self.label);
}
function onClick() {
alert('*click*');
}
},
link: function myButtonPostLink(scope, elem, attrs, ctrl) {
ctrl._setupElement();
}
};
})
Dopo v1.5.x
.component('myButton', {
template: '<button ng-click="$ctrl.onClick()></button>',
bindings: {
label: '@'
}
controller: function MyButtonController($element) {
// Variables - Private
var self = this;
// Functions - Public
self.$postLink = $postLink;
self.onClick = onClick;
// Functions - Definitions
function $postLink() {
$element.text(self.label);
}
function onClick() {
alert('*click*');
}
}
})
Si prega di includere un esempio – devqon
@devqon Ho aggiunto un esempio di come scriviamo una direttiva personalizzata che contiene la definizione di funzione che verrà chiamata dal controller.La funzione può fare qualsiasi cosa dall'aggiunta di listener di eventi alla manipolazione del DOM, ecc. –