AngularJS
Direktiv som använder ngModelController
Sök…
En enkel kontroll: betyg
Låt oss bygga en enkel kontroll, en klassificeringswidget, avsedd att användas som:
<rating min="0" max="5" nullifier="true" ng-model="data.rating"></rating>
Ingen snygg CSS för tillfället; detta skulle återges som:
0 1 2 3 4 5 x
Om du klickar på ett nummer väljer du den graden; och klicka på "x" sätter betyg till noll.
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>'
};
});
Nyckelord:
- Implementera
ngModel.$render
att överföra modellens visningsvärde till din vy. - Ring
ngModel.$setViewValue()
när du känner att visningsvärdet bör uppdateras. - Styrningen kan naturligtvis parametreras; använd
'<'
omfångsbindningar för parametrar, om i Angular> = 1.5 för att tydligt indikera inmatning - envägsbindning. Om du måste vidta åtgärder när en parameter ändras kan du använda en JavaScript-egenskap (seObject.defineProperty()
) för att spara några klockor.
Obs 1: För att inte komplicera implementeringen ctrl.options
klassificeringsvärdena i en matris - ctrl.options
. Detta behövs inte; en mer effektiv, men också mer komplex implementering kan använda DOM-manipulation för att infoga / ta bort betyg när min
/ max
ändring.
Obs 2: Med undantag för '<'
omfattningsbindningar kan detta exempel användas i vinkel <1,5. Om du är på Angular> = 1.5, skulle det vara en bra idé att överföra detta till en komponent och använda $onInit()
livscykelkroken för att initialisera min
och max
, istället för att göra det i kontrollerns konstruktör.
Och en nödvändig fiol: https://jsfiddle.net/h81mgxma/
Ett par komplexa kontroller: redigera ett fullständigt objekt
En anpassad kontroll behöver inte begränsa sig till triviala saker som primitiva; det kan redigera mer intressanta saker. Här presenterar vi två typer av anpassade kontroller, en för redigering av personer och en för redigering av adresser. Adresskontrollen används för att redigera personens adress. Ett exempel på användning skulle vara:
<input-person ng-model="data.thePerson"></input-person>
<input-address ng-model="data.thePerson.address"></input-address>
Modellen för detta exempel är medvetet förenklad:
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;
}
Adressredigeraren:
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>'
};
});
Nyckelord:
- Vi redigerar ett objekt; vi vill inte ändra objektet som vi har fått direkt från vår förälder (vi vill att vår modell ska vara förenlig med immutabilitetsprincipen). Så vi skapar en grund klocka på objektet som redigeras och uppdaterar modellen med
$setViewValue()
varje gång en egenskap ändras. Vi skickar en kopia till vår förälder. - När modellen ändras från utsidan kopierar vi den och sparar kopian till vårt omfattning. Immutabilitetsprinciper igen, även om den interna kopian inte är oföränderlig, det externa kan mycket väl vara. Dessutom bygger vi om klockan (
this_unwatch();this._makeWatch();
), för att undvika att utlösa vaktaren för ändringar som drivs till oss av modellen. (Vi vill bara att klockan ska aktiveras för ändringar som görs i UI.) - Annat att punkterna ovan implementerar vi
ngModel.$render()
och kallarngModel.$setViewValue()
som vi skulle göra för en enkel kontroll (se betygsexemplet).
Koden för personens anpassade kontroll är nästan identisk. Mallen använder <input-address>
. I en mer avancerad implementering kunde vi extrahera kontrollerna i en återanvändbar modul.
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>'
};
});
Obs: Här skrivs objekten, dvs de har rätt konstruktörer. Detta är inte obligatoriskt; modellen kan vara vanliga JSON-objekt. I det här fallet använder du bara angular.copy()
istället för konstruktörerna. En ytterligare fördel är att styrenheten blir identisk för de två kontrollerna och lätt kan extraheras till någon gemensam modul.
Fiolen: https://jsfiddle.net/3tzyqfko/2/
Två versioner av fiolen som har extraherat den gemensamma koden för kontrollerna: https://jsfiddle.net/agj4cp0e/ och https://jsfiddle.net/ugb6Lw8b/