Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit ba0e9fe

Browse files
Florian Scholzjelbourn
authored andcommitted
feat(md-chips): added validation for ng-required (#11125)
Fixes #11124
1 parent 2ef87f4 commit ba0e9fe

File tree

6 files changed

+200
-11
lines changed

6 files changed

+200
-11
lines changed

src/components/chips/chips.spec.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,59 @@ describe('<md-chips>', function() {
10441044

10451045
});
10461046

1047+
describe('ng-required', function() {
1048+
beforeEach(function() {
1049+
// Clear default items to test the required chips functionality
1050+
scope.items = [];
1051+
});
1052+
1053+
it('should set the required error when chips is compiled with an empty array', function() {
1054+
var template =
1055+
'<form name="form">' +
1056+
'<md-chips name="chips" ng-required="true" ng-model="items"></md-chips>' +
1057+
'</form>';
1058+
1059+
var element = buildChips(template);
1060+
element.scope().$apply();
1061+
1062+
expect(scope.form.chips.$error['required']).toBe(true);
1063+
});
1064+
1065+
it('should unset the required error when the first chip is added', function() {
1066+
var template =
1067+
'<form name="form">' +
1068+
'<md-chips name="chips" ng-required="true" ng-model="items"></md-chips>' +
1069+
'</form>';
1070+
1071+
var element = buildChips(template);
1072+
var ctrl = element.find('md-chips').controller('mdChips');
1073+
1074+
element.scope().$apply(function() {
1075+
ctrl.chipBuffer = 'Test';
1076+
simulateInputEnterKey(ctrl);
1077+
});
1078+
1079+
expect(scope.form.chips.$error['required']).toBeUndefined();
1080+
});
1081+
1082+
it('should set the required when the last chip is removed', function() {
1083+
scope.items = ['test'];
1084+
var template =
1085+
'<form name="form">' +
1086+
'<md-chips name="chips" required ng-model="items"></md-chips>' +
1087+
'</form>';
1088+
1089+
var element = buildChips(template);
1090+
var ctrl = element.find('md-chips').controller('mdChips');
1091+
1092+
element.scope().$apply(function() {
1093+
ctrl.removeChip(0);
1094+
});
1095+
1096+
expect(scope.form.chips.$error['required']).toBe(true);
1097+
});
1098+
});
1099+
10471100
describe('focus functionality', function() {
10481101
var element, ctrl;
10491102

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<div ng-controller="ChipsValidationCtrl as ctrl" layout="column" ng-cloak>
2+
<md-content class="md-padding" layout="column">
3+
4+
<h2 class="md-title">Required</h2>
5+
<form name="fruitForm">
6+
<md-chips name="fruits"
7+
ng-model="ctrl.selectedFruit"
8+
ng-required="true"
9+
placeholder="Add a fruit">
10+
</md-chips>
11+
<div class="md-chips-messages"
12+
ng-show="fruitForm.fruits.$dirty || fruitForm.$submitted"
13+
ng-messages="fruitForm.fruits.$error">
14+
<div ng-message="required">At least one fruit is required</div>
15+
</div>
16+
</form>
17+
18+
19+
<h2 class="md-title">Max Chips</h2>
20+
<form name="vegetableForm">
21+
<md-chips name="vegetables"
22+
ng-model="ctrl.selectedVegetables"
23+
placeholder="Add a vegetable"
24+
md-max-chips="5">
25+
</md-chips>
26+
<div class="md-chips-messages"
27+
ng-show="vegetableForm.vegetables.$dirty || vegetableForm.$submitted"
28+
ng-messages="vegetableForm.vegetables.$error">
29+
<div ng-message="md-max-chips">You reached the maximum number of vegetables</div>
30+
</div>
31+
</form>
32+
33+
<p class="note">
34+
Be aware that error messages for chips are not styled by default since they are not part of <code>md-input-container</code>.
35+
</p>
36+
37+
</md-content>
38+
</div>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(function () {
2+
'use strict';
3+
angular
4+
.module('chipsValidationDemo', ['ngMaterial', 'ngMessages'])
5+
.controller('ChipsValidationCtrl', ValidationCtrl);
6+
7+
function ValidationCtrl () {
8+
this.selectedFruit = [];
9+
this.selectedVegetables = [];
10+
}
11+
})();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.md-chips-messages {
2+
color: rgb(221,44,0);
3+
position: relative;
4+
order: 4;
5+
overflow: hidden;
6+
}
7+
8+
.md-chips-messages [ng-message] {
9+
font-size: 12px;
10+
line-height: 14px;
11+
overflow: hidden;
12+
margin-top: 0;
13+
padding-top: 5px;
14+
}
15+
16+
p.note {
17+
font-size: 12px;
18+
}

src/components/chips/js/chipsController.js

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $md
160160
*/
161161
this.chipAppendDelay = DEFAULT_CHIP_APPEND_DELAY;
162162

163+
/**
164+
* Collection of functions to call to un-register watchers
165+
*
166+
* @type {Array}
167+
*/
168+
this.deRegister = [];
169+
163170
this.init();
164171
}
165172

@@ -170,18 +177,32 @@ MdChipsCtrl.prototype.init = function() {
170177
var ctrl = this;
171178

172179
// Set the wrapper ID
173-
ctrl.wrapperId = '_md-chips-wrapper-' + ctrl.$mdUtil.nextUid();
180+
this.wrapperId = '_md-chips-wrapper-' + this.$mdUtil.nextUid();
174181

175182
// Setup a watcher which manages the role and aria-owns attributes
176-
ctrl.$scope.$watchCollection('$mdChipsCtrl.items', function() {
177-
// Make sure our input and wrapper have the correct ARIA attributes
178-
ctrl.setupInputAria();
179-
ctrl.setupWrapperAria();
180-
});
183+
this.deRegister.push(
184+
this.$scope.$watchCollection('$mdChipsCtrl.items', function() {
185+
// Make sure our input and wrapper have the correct ARIA attributes
186+
ctrl.setupInputAria();
187+
ctrl.setupWrapperAria();
188+
})
189+
);
181190

182-
ctrl.$attrs.$observe('mdChipAppendDelay', function(newValue) {
183-
ctrl.chipAppendDelay = parseInt(newValue) || DEFAULT_CHIP_APPEND_DELAY;
184-
});
191+
this.deRegister.push(
192+
this.$attrs.$observe('mdChipAppendDelay', function(newValue) {
193+
ctrl.chipAppendDelay = parseInt(newValue) || DEFAULT_CHIP_APPEND_DELAY;
194+
})
195+
);
196+
};
197+
198+
/**
199+
* Destructor for cleanup
200+
*/
201+
MdChipsCtrl.prototype.$onDestroy = function $onDestroy() {
202+
var $destroyFn;
203+
while (($destroyFn = this.deRegister.pop())) {
204+
$destroyFn.call(this);
205+
}
185206
};
186207

187208
/**
@@ -560,6 +581,7 @@ MdChipsCtrl.prototype.hasMaxChipsReached = function() {
560581
*/
561582
MdChipsCtrl.prototype.validateModel = function() {
562583
this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
584+
this.ngModelCtrl.$validate(); // rerun any registered validators
563585
};
564586

565587
/**
@@ -685,6 +707,12 @@ MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
685707
this.ngModelCtrl = ngModelCtrl;
686708

687709
var self = this;
710+
711+
// in chips the meaning of $isEmpty changes
712+
ngModelCtrl.$isEmpty = function(value) {
713+
return !value || value.length === 0;
714+
};
715+
688716
ngModelCtrl.$render = function() {
689717
// model is updated. do something.
690718
self.items = self.ngModelCtrl.$viewValue;
@@ -716,6 +744,43 @@ MdChipsCtrl.prototype.onInputBlur = function () {
716744
}
717745
};
718746

747+
/**
748+
* Configure event bindings on input element.
749+
* @param inputElement
750+
*/
751+
MdChipsCtrl.prototype.configureInput = function configureInput(inputElement) {
752+
// Find the NgModelCtrl for the input element
753+
var ngModelCtrl = inputElement.controller('ngModel');
754+
var ctrl = this;
755+
756+
if (ngModelCtrl) {
757+
758+
// sync touched-state from inner input to chips-element
759+
this.deRegister.push(
760+
this.$scope.$watch(
761+
function() {
762+
return ngModelCtrl.$touched;
763+
},
764+
function(isTouched) {
765+
isTouched && ctrl.ngModelCtrl.$setTouched();
766+
}
767+
)
768+
);
769+
770+
// sync dirty-state from inner input to chips-element
771+
this.deRegister.push(
772+
this.$scope.$watch(
773+
function() {
774+
return ngModelCtrl.$dirty;
775+
},
776+
function(isDirty) {
777+
isDirty && ctrl.ngModelCtrl.$setDirty();
778+
}
779+
)
780+
);
781+
}
782+
};
783+
719784
/**
720785
* Configure event bindings on a user-provided input element.
721786
* @param inputElement
@@ -743,7 +808,7 @@ MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
743808
.attr({ tabindex: 0 })
744809
.on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) })
745810
.on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) })
746-
.on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) })
811+
.on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) });
747812
};
748813

749814
MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {

src/components/chips/js/chipsDirective.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
* @param {string=} md-enable-chip-edit Set this to "true" to enable editing of chip contents. The user can
110110
* go into edit mode with pressing "space", "enter", or double clicking on the chip. Chip edit is only
111111
* supported for chips with basic template.
112+
* @param {boolean=} ng-required Whether ng-model is allowed to be empty or not.
112113
* @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
113114
* <br/><br/>The validation property `md-max-chips` can be used when the max chips
114115
* amount is reached.
@@ -421,7 +422,10 @@
421422
$mdUtil.nextTick(function() {
422423
var input = element.find('input');
423424

424-
input && input.toggleClass('md-input', true);
425+
if (input) {
426+
mdChipsCtrl.configureInput(input);
427+
input.toggleClass('md-input', true);
428+
}
425429
});
426430
}
427431

0 commit comments

Comments
 (0)