AngularJS
Directives utilisant ngModelController
Recherche…
Un contrôle simple: note
Construisons un contrôle simple, un widget d'évaluation, destiné à être utilisé comme:
<rating min="0" max="5" nullifier="true" ng-model="data.rating"></rating>
Pas de CSS sophistiqué pour l'instant; cela se traduirait par:
0 1 2 3 4 5 x
En cliquant sur un nombre, vous sélectionnez cette cote. et en cliquant sur le "x", la note est définie sur 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>'
};
});
Points clés:
- Implémentez
ngModel.$render
pour transférer la valeur de vue du modèle à votre vue. - Appelez
ngModel.$setViewValue()
chaque fois que vous estimez que la valeur de la vue doit être mise à jour. - Le contrôle peut bien entendu être paramétré; utilisez
'<'
liaisons de portée'<'
pour les paramètres, si dans Angular> = 1.5 pour indiquer clairement l'entrée - liaison unidirectionnelle. Si vous devez agir chaque fois qu'un paramètre change, vous pouvez utiliser une propriété JavaScript (voirObject.defineProperty()
) pour enregistrer quelques montres.
Note 1: Afin de ne pas compliquer l'implémentation, les valeurs de notation sont insérées dans un tableau - les ctrl.options
. Ce n'est pas nécessaire une implémentation plus efficace, mais aussi plus complexe, pourrait utiliser la manipulation DOM pour insérer / supprimer des notations lorsque le changement min
/ max
.
Remarque 2: À l'exception des liaisons de portée '<'
, cet exemple peut être utilisé dans Angular <1.5. Si vous êtes sur Angular> = 1.5, ce serait une bonne idée de transformer ceci en composant et d'utiliser le hook de cycle de vie $onInit()
pour initialiser min
et max
, au lieu de le faire dans le constructeur du contrôleur.
Et un violon nécessaire: https://jsfiddle.net/h81mgxma/
Un couple de contrôles complexes: éditer un objet complet
Un contrôle personnalisé ne doit pas se limiter à des choses triviales comme les primitives; il peut éditer des choses plus intéressantes. Nous présentons ici deux types de contrôles personnalisés, l'un pour l'édition des personnes et l'autre pour l'édition des adresses. Le contrôle d'adresse est utilisé pour modifier l'adresse de la personne. Un exemple d'utilisation serait:
<input-person ng-model="data.thePerson"></input-person>
<input-address ng-model="data.thePerson.address"></input-address>
Le modèle de cet exemple est volontairement simpliste:
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'éditeur d'adresse:
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>'
};
});
Points clés:
- Nous modifions un objet; nous ne voulons pas changer directement l'objet donné par notre société mère (nous voulons que notre modèle soit compatible avec le principe d'immuabilité). Nous créons donc une montre peu profonde sur l'objet en cours d'édition et
$setViewValue()
jour le modèle avec$setViewValue()
chaque fois qu'une propriété change. Nous transmettons une copie à notre parent. - Chaque fois que le modèle change de l'extérieur, nous le copions et sauvegardons la copie dans notre champ d'application. Les principes d'immuabilité encore une fois, bien que la copie interne ne soit pas immuable, l'externe pourrait très bien l'être. De plus, nous reconstruisons la montre (
this_unwatch();this._makeWatch();
), pour éviter que l’observateur ne subisse les modifications que le modèle nous athis_unwatch();this._makeWatch();
. (Nous voulons seulement que la montre se déclenche pour les modifications apportées dans l'interface utilisateur.) - Autre que les points ci-dessus, nous implémentons
ngModel.$render()
et appelonsngModel.$setViewValue()
comme nous le ferions pour un contrôle simple (voir l'exemple de notation).
Le code du contrôle personnalisé de la personne est presque identique. Le modèle utilise <input-address>
. Dans une implémentation plus avancée, nous pourrions extraire les contrôleurs dans un module réutilisable.
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>'
};
});
Note: Ici, les objets sont typés, c’est-à-dire qu’ils ont des constructeurs appropriés. Ce n'est pas obligatoire le modèle peut être des objets JSON simples. Dans ce cas, utilisez simplement angular.copy()
place des constructeurs. Un avantage supplémentaire est que le contrôleur devient identique pour les deux contrôles et peut facilement être extrait dans un module commun.
Le violon: https://jsfiddle.net/3tzyqfko/2/
Deux versions du violon ayant extrait le code commun des contrôleurs: https://jsfiddle.net/agj4cp0e/ et https://jsfiddle.net/ugb6Lw8b/