AngularJS
Direttive che utilizzano ngModelController
Ricerca…
Un semplice controllo: valutazione
Costruiamo un semplice controllo, un widget di valutazione, destinato a essere utilizzato come:
<rating min="0" max="5" nullifier="true" ng-model="data.rating"></rating>
Per ora non ci sono CSS fantasiosi; questo renderebbe come:
0 1 2 3 4 5 x
Cliccando su un numero seleziona quella valutazione; e facendo clic su "x" imposta la valutazione su null.
app.directive('rating', function() {
function RatingController() {
this._ngModel = null;
this.rating = null;
this.options = null;
this.min = typeof this.min === 'number' ? this.min : 1;
this.max = typeof this.max === 'number' ? this.max : 5;
}
RatingController.prototype.setNgModel = function(ngModel) {
this._ngModel = ngModel;
if( ngModel ) {
// KEY POINT 1
ngModel.$render = this._render.bind(this);
}
};
RatingController.prototype._render = function() {
this.rating = this._ngModel.$viewValue != null ? this._ngModel.$viewValue : -Number.MAX_VALUE;
};
RatingController.prototype._calculateOptions = function() {
if( this.min == null || this.max == null ) {
this.options = [];
}
else {
this.options = new Array(this.max - this.min + 1);
for( var i=0; i < this.options.length; i++ ) {
this.options[i] = this.min + i;
}
}
};
RatingController.prototype.setValue = function(val) {
this.rating = val;
// KEY POINT 2
this._ngModel.$setViewValue(val);
};
// KEY POINT 3
Object.defineProperty(RatingController.prototype, 'min', {
get: function() {
return this._min;
},
set: function(val) {
this._min = val;
this._calculateOptions();
}
});
Object.defineProperty(RatingController.prototype, 'max', {
get: function() {
return this._max;
},
set: function(val) {
this._max = val;
this._calculateOptions();
}
});
return {
restrict: 'E',
scope: {
// KEY POINT 3
min: '<?',
max: '<?',
nullifier: '<?'
},
bindToController: true,
controllerAs: 'ctrl',
controller: RatingController,
require: ['rating', 'ngModel'],
link: function(scope, elem, attrs, ctrls) {
ctrls[0].setNgModel(ctrls[1]);
},
template:
'<span ng-repeat="o in ctrl.options" href="#" class="rating-option" ng-class="{\'rating-option-active\': o <= ctrl.rating}" ng-click="ctrl.setValue(o)">{{ o }}</span>' +
'<span ng-if="ctrl.nullifier" ng-click="ctrl.setValue(null)" class="rating-nullifier">✖</span>'
};
});
Punti chiave:
- Implementa
ngModel.$render
per trasferire il valore della vista del modello alla tua vista. - Chiama
ngModel.$setViewValue()
ogni volta che ritieni che il valore della vista debba essere aggiornato. - Il controllo può naturalmente essere parametrizzato; utilizzare
'<'
scope scope per i parametri, se in Angular> = 1.5 per indicare chiaramente input - unidirezionale. Se devi agire ogni volta che un parametro cambia, puoi usare una proprietà JavaScript (vediObject.defineProperty()
) per salvare alcuni orologi.
Nota 1: per non complicare eccessivamente l'implementazione, i valori di valutazione sono inseriti in una matrice: ctrl.options
. Questo non è necessario; Un'implementazione più efficiente, ma anche più complessa, potrebbe utilizzare la manipolazione del DOM per inserire / rimuovere le valutazioni quando si modifica min
/ max
.
Nota 2: con l'eccezione delle associazioni di ambito '<'
, questo esempio può essere utilizzato in Angular <1.5. Se si utilizza Angular> = 1.5, sarebbe una buona idea trasformarlo in un componente e utilizzare l'hook del ciclo di vita $onInit()
per inizializzare min
e max
, invece di farlo nel costruttore del controllore.
E un violino necessario: https://jsfiddle.net/h81mgxma/
Un paio di controlli complessi: modifica un oggetto completo
Un controllo personalizzato non deve limitarsi a cose banali come i primitivi; può modificare cose più interessanti. Qui presentiamo due tipi di controlli personalizzati, uno per la modifica delle persone e uno per la modifica degli indirizzi. Il controllo dell'indirizzo viene utilizzato per modificare l'indirizzo della persona. Un esempio di utilizzo sarebbe:
<input-person ng-model="data.thePerson"></input-person>
<input-address ng-model="data.thePerson.address"></input-address>
Il modello per questo esempio è volutamente semplicistico:
function Person(data) {
data = data || {};
this.name = data.name;
this.address = data.address ? new Address(data.address) : null;
}
function Address(data) {
data = data || {};
this.street = data.street;
this.number = data.number;
}
L'editor di indirizzi:
app.directive('inputAddress', function() {
InputAddressController.$inject = ['$scope'];
function InputAddressController($scope) {
this.$scope = $scope;
this._ngModel = null;
this.value = null;
this._unwatch = angular.noop;
}
InputAddressController.prototype.setNgModel = function(ngModel) {
this._ngModel = ngModel;
if( ngModel ) {
// KEY POINT 3
ngModel.$render = this._render.bind(this);
}
};
InputAddressController.prototype._makeWatch = function() {
// KEY POINT 1
this._unwatch = this.$scope.$watchCollection(
(function() {
return this.value;
}).bind(this),
(function(newval, oldval) {
if( newval !== oldval ) { // skip the initial trigger
this._ngModel.$setViewValue(newval !== null ? new Address(newval) : null);
}
}).bind(this)
);
};
InputAddressController.prototype._render = function() {
// KEY POINT 2
this._unwatch();
this.value = this._ngModel.$viewValue ? new Address(this._ngModel.$viewValue) : null;
this._makeWatch();
};
return {
restrict: 'E',
scope: {},
bindToController: true,
controllerAs: 'ctrl',
controller: InputAddressController,
require: ['inputAddress', 'ngModel'],
link: function(scope, elem, attrs, ctrls) {
ctrls[0].setNgModel(ctrls[1]);
},
template:
'<div>' +
'<label><span>Street:</span><input type="text" ng-model="ctrl.value.street" /></label>' +
'<label><span>Number:</span><input type="text" ng-model="ctrl.value.number" /></label>' +
'</div>'
};
});
Punti chiave:
- Stiamo modificando un oggetto; non vogliamo cambiare direttamente l'oggetto che ci è stato dato dal nostro genitore (vogliamo che il nostro modello sia compatibile con il principio dell'immutabilità). Quindi creiamo una vigilanza superficiale sull'oggetto da modificare e aggiorniamo il modello con
$setViewValue()
ogni volta che una proprietà cambia. Passiamo una copia al nostro genitore. - Ogni volta che il modello cambia dall'esterno, lo copiamo e salviamo la copia nel nostro ambito. Ancora principi di immutabilità, sebbene la copia interna non sia immutabile, l'esterno potrebbe benissimo essere. Inoltre ricostruiamo l'orologio (
this_unwatch();this._makeWatch();
), per evitare di far scattare l'osservatore per le modifiche che il modello ci ha spinto. (Vogliamo solo che l'orologio si attivi per le modifiche apportate all'interfaccia utente.) - A parte i punti precedenti, implementiamo
ngModel.$render()
e chiamiamongModel.$setViewValue()
come faremmo per un controllo semplice (vedere l'esempio di valutazione).
Il codice per il controllo personalizzato della persona è quasi identico. Il modello sta usando <input-address>
. In un'implementazione più avanzata potremmo estrarre i controller in un modulo riutilizzabile.
app.directive('inputPerson', function() {
InputPersonController.$inject = ['$scope'];
function InputPersonController($scope) {
this.$scope = $scope;
this._ngModel = null;
this.value = null;
this._unwatch = angular.noop;
}
InputPersonController.prototype.setNgModel = function(ngModel) {
this._ngModel = ngModel;
if( ngModel ) {
ngModel.$render = this._render.bind(this);
}
};
InputPersonController.prototype._makeWatch = function() {
this._unwatch = this.$scope.$watchCollection(
(function() {
return this.value;
}).bind(this),
(function(newval, oldval) {
if( newval !== oldval ) { // skip the initial trigger
this._ngModel.$setViewValue(newval !== null ? new Person(newval) : null);
}
}).bind(this)
);
};
InputPersonController.prototype._render = function() {
this._unwatch();
this.value = this._ngModel.$viewValue ? new Person(this._ngModel.$viewValue) : null;
this._makeWatch();
};
return {
restrict: 'E',
scope: {},
bindToController: true,
controllerAs: 'ctrl',
controller: InputPersonController,
require: ['inputPerson', 'ngModel'],
link: function(scope, elem, attrs, ctrls) {
ctrls[0].setNgModel(ctrls[1]);
},
template:
'<div>' +
'<label><span>Name:</span><input type="text" ng-model="ctrl.value.name" /></label>' +
'<input-address ng-model="ctrl.value.address"></input-address>' +
'</div>'
};
});
Nota: qui gli oggetti sono digitati, cioè hanno costruttori adeguati. Questo non è obbligatorio; il modello può essere semplice oggetti JSON. In questo caso basta usare angular.copy()
posto dei costruttori. Un ulteriore vantaggio è che il controller diventa identico per i due controlli e può essere facilmente estratto in alcuni moduli comuni.
Il violino: https://jsfiddle.net/3tzyqfko/2/
Due versioni del violino hanno estratto il codice comune dei controller: https://jsfiddle.net/agj4cp0e/ e https://jsfiddle.net/ugb6Lw8b/