2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ng.material.components.input');
8 goog.require('ng.material.core');
11 * @name material.components.input
14 angular.module('material.components.input', [
17 .directive('mdInputContainer', mdInputContainerDirective)
18 .directive('label', labelDirective)
19 .directive('input', inputTextareaDirective)
20 .directive('textarea', inputTextareaDirective)
21 .directive('mdMaxlength', mdMaxlengthDirective)
22 .directive('placeholder', placeholderDirective);
26 * @name mdInputContainer
27 * @module material.components.input
32 * `<md-input-container>` is the parent of any input or textarea element.
34 * Input and textarea elements will not behave properly unless the md-input-container
37 * @param md-is-error {expression=} When the given expression evaluates to true, the input container will go into error state. Defaults to erroring if the input has been touched and is invalid.
38 * @param md-no-float {boolean=} When present, placeholders will not be converted to floating labels
43 * <md-input-container>
44 * <label>Username</label>
45 * <input type="text" ng-model="user.name">
46 * </md-input-container>
48 * <md-input-container>
49 * <label>Description</label>
50 * <textarea ng-model="user.description"></textarea>
51 * </md-input-container>
55 function mdInputContainerDirective($mdTheming, $parse) {
56 ContainerCtrl.$inject = ["$scope", "$element", "$attrs"];
60 controller: ContainerCtrl
63 function postLink(scope, element, attr) {
66 function ContainerCtrl($scope, $element, $attrs) {
69 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
71 self.delegateClick = function() {
74 self.element = $element;
75 self.setFocused = function(isFocused) {
76 $element.toggleClass('md-input-focused', !!isFocused);
78 self.setHasValue = function(hasValue) {
79 $element.toggleClass('md-input-has-value', !!hasValue);
81 self.setInvalid = function(isInvalid) {
82 $element.toggleClass('md-input-invalid', !!isInvalid);
84 $scope.$watch(function() {
85 return self.label && self.input;
86 }, function(hasLabelAndInput) {
87 if (hasLabelAndInput && !self.label.attr('for')) {
88 self.label.attr('for', self.input.attr('id'));
93 mdInputContainerDirective.$inject = ["$mdTheming", "$parse"];
95 function labelDirective() {
98 require: '^?mdInputContainer',
99 link: function(scope, element, attr, containerCtrl) {
100 if (!containerCtrl || attr.mdNoFloat) return;
102 containerCtrl.label = element;
103 scope.$on('$destroy', function() {
104 containerCtrl.label = null;
114 * @module material.components.input
117 * Use the `<input>` or the `<textarea>` as a child of an `<md-input-container>`.
119 * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is specified, a character counter will be shown underneath the input.<br/><br/>
120 * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength` or maxlength attributes.
121 * @param {string=} aria-label Aria-label is required when no label is present. A warning message will be logged in the console if not present.
122 * @param {string=} placeholder An alternative approach to using aria-label when the label is not present. The placeholder text is copied to the aria-label attribute.
126 * <md-input-container>
127 * <label>Color</label>
128 * <input type="text" ng-model="color" required md-maxlength="10">
129 * </md-input-container>
131 * <h3>With Errors</h3>
134 * <form name="userForm">
135 * <md-input-container>
136 * <label>Last Name</label>
137 * <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4">
138 * <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty">
139 * <div ng-message="required">This is required!</div>
140 * <div ng-message="md-maxlength">That's too long!</div>
141 * <div ng-message="minlength">That's too short!</div>
143 * </md-input-container>
144 * <md-input-container>
145 * <label>Biography</label>
146 * <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea>
147 * <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty">
148 * <div ng-message="required">This is required!</div>
149 * <div ng-message="md-maxlength">That's too long!</div>
151 * </md-input-container>
152 * <md-input-container>
153 * <input aria-label='title' ng-model='title'>
154 * </md-input-container>
155 * <md-input-container>
156 * <input placeholder='title' ng-model='title'>
157 * </md-input-container>
161 * Requires [ngMessages](https://docs.angularjs.org/api/ngMessages).
162 * Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input).
166 function inputTextareaDirective($mdUtil, $window, $mdAria) {
169 require: ['^?mdInputContainer', '?ngModel'],
173 function postLink(scope, element, attr, ctrls) {
175 var containerCtrl = ctrls[0];
176 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
177 var isReadonly = angular.isDefined(attr.readonly);
179 if ( !containerCtrl ) return;
180 if (containerCtrl.input) {
181 throw new Error("<md-input-container> can only have *one* <input> or <textarea> child element!");
183 containerCtrl.input = element;
185 if(!containerCtrl.label) {
186 $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
189 element.addClass('md-input');
190 if (!element.attr('id')) {
191 element.attr('id', 'input_' + $mdUtil.nextUid());
194 if (element[0].tagName.toLowerCase() === 'textarea') {
198 var isErrorGetter = containerCtrl.isErrorGetter || function() {
199 return ngModelCtrl.$invalid && ngModelCtrl.$touched;
201 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
203 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
204 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
206 element.on('input', inputCheckValue);
210 .on('focus', function(ev) {
211 containerCtrl.setFocused(true);
213 .on('blur', function(ev) {
214 containerCtrl.setFocused(false);
220 //ngModelCtrl.$setTouched();
221 //if( ngModelCtrl.$invalid ) containerCtrl.setInvalid();
223 scope.$on('$destroy', function() {
224 containerCtrl.setFocused(false);
225 containerCtrl.setHasValue(false);
226 containerCtrl.input = null;
232 function ngModelPipelineCheckValue(arg) {
233 containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
236 function inputCheckValue() {
237 // An input's value counts if its length > 0,
238 // or if the input's validity state says it has bad input (eg string in a number input)
239 containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity||{}).badInput);
242 function setupTextarea() {
243 var node = element[0];
244 var onChangeTextarea = $mdUtil.debounce(growTextarea, 1);
246 function pipelineListener(value) {
252 ngModelCtrl.$formatters.push(pipelineListener);
253 ngModelCtrl.$viewChangeListeners.push(pipelineListener);
257 element.on('keydown input', onChangeTextarea);
258 element.on('scroll', onScroll);
259 angular.element($window).on('resize', onChangeTextarea);
261 scope.$on('$destroy', function() {
262 angular.element($window).off('resize', onChangeTextarea);
265 function growTextarea() {
266 node.style.height = "auto";
268 var height = getHeight();
269 if (height) node.style.height = height + 'px';
272 function getHeight () {
273 var line = node.scrollHeight - node.offsetHeight;
274 return node.offsetHeight + (line > 0 ? line : 0);
277 function onScroll(e) {
279 // for smooth new line adding
280 var line = node.scrollHeight - node.offsetHeight;
281 var height = node.offsetHeight + line;
282 node.style.height = height + 'px';
287 inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria"];
289 function mdMaxlengthDirective($animate) {
292 require: ['ngModel', '^mdInputContainer'],
296 function postLink(scope, element, attr, ctrls) {
298 var ngModelCtrl = ctrls[0];
299 var containerCtrl = ctrls[1];
300 var charCountEl = angular.element('<div class="md-char-counter">');
302 // Stop model from trimming. This makes it so whitespace
303 // over the maxlength still counts as invalid.
304 attr.$set('ngTrim', 'false');
305 containerCtrl.element.append(charCountEl);
307 ngModelCtrl.$formatters.push(renderCharCount);
308 ngModelCtrl.$viewChangeListeners.push(renderCharCount);
309 element.on('input keydown', function() {
310 renderCharCount(); //make sure it's called with no args
313 scope.$watch(attr.mdMaxlength, function(value) {
315 if (angular.isNumber(value) && value > 0) {
316 if (!charCountEl.parent().length) {
317 $animate.enter(charCountEl, containerCtrl.element,
318 angular.element(containerCtrl.element[0].lastElementChild));
322 $animate.leave(charCountEl);
326 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
327 if (!angular.isNumber(maxlength) || maxlength < 0) {
330 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
333 function renderCharCount(value) {
334 charCountEl.text( ( element.val() || value || '' ).length + '/' + maxlength );
339 mdMaxlengthDirective.$inject = ["$animate"];
341 function placeholderDirective($log) {
342 var blackListElements = ['MD-SELECT'];
345 require: '^^?mdInputContainer',
350 function postLink(scope, element, attr, inputContainer) {
351 if (!inputContainer) return;
352 if (blackListElements.indexOf(element[0].nodeName) != -1) return;
353 if (angular.isDefined(inputContainer.element.attr('md-no-float'))) return;
355 var placeholderText = attr.placeholder;
356 element.removeAttr('placeholder');
358 if ( inputContainer.element.find('label').length == 0 ) {
359 var placeholder = '<label ng-click="delegateClick()">' + placeholderText + '</label>';
361 inputContainer.element.addClass('md-icon-float');
362 inputContainer.element.prepend(placeholder);
364 $log.warn("The placeholder='" + placeholderText + "' will be ignored since this md-input-container has a child label element.");
369 placeholderDirective.$inject = ["$log"];
371 ng.material.components.input = angular.module("material.components.input");