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

Commit debc3a9

Browse files
committed
feat(mdInput): Add support for both labels and placeholders.
Previously, if a user supplied both a placeholder and a label, the label would float on top of the placeholder when the input did not have a value. Fix by adding styles/code to support both at the same time. **Note:** If the users provides both a label and a placeholder, the label will no longer animate. Also fix input styles so transition does not happen if input already has a value to avoid unneccessary and eratic-looking animations. Fixes #4462. Fixes #4258.
1 parent c7f2d64 commit debc3a9

File tree

4 files changed

+88
-56
lines changed

4 files changed

+88
-56
lines changed

src/components/input/demoBasicUsage/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
</md-input-container>
5454
<md-input-container flex>
5555
<label>Postal Code</label>
56-
<input ng-model="user.postalCode">
56+
<input ng-model="user.postalCode" placeholder="12345">
5757
</md-input-container>
5858
</div>
5959

src/components/input/input.js

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ angular.module('material.components.input', [
2626
* Input and textarea elements will not behave properly unless the md-input-container
2727
* parent is provided.
2828
*
29-
* @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.
30-
* @param md-no-float {boolean=} When present, placeholders will not be converted to floating labels
29+
* @param md-is-error {expression=} When the given expression evaluates to true, the input container
30+
* will go into error state. Defaults to erroring if the input has been touched and is invalid.
31+
* @param md-no-float {boolean=} When present, placeholders will not be converted to floating
32+
* labels.
3133
*
3234
* @usage
3335
* <hljs lang="html">
@@ -54,6 +56,7 @@ function mdInputContainerDirective($mdTheming, $parse) {
5456
function postLink(scope, element, attr) {
5557
$mdTheming(element);
5658
}
59+
5760
function ContainerCtrl($scope, $element, $attrs) {
5861
var self = this;
5962

@@ -69,6 +72,9 @@ function mdInputContainerDirective($mdTheming, $parse) {
6972
self.setHasValue = function(hasValue) {
7073
$element.toggleClass('md-input-has-value', !!hasValue);
7174
};
75+
self.setHasPlaceholder = function(hasPlaceholder) {
76+
$element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
77+
};
7278
self.setInvalid = function(isInvalid) {
7379
$element.toggleClass('md-input-invalid', !!isInvalid);
7480
};
@@ -106,10 +112,15 @@ function labelDirective() {
106112
* @description
107113
* Use the `<input>` or the `<textarea>` as a child of an `<md-input-container>`.
108114
*
109-
* @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/>
110-
* 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.
111-
* @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.
112-
* @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.
115+
* @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is
116+
* specified, a character counter will be shown underneath the input.<br/><br/>
117+
* The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't
118+
* want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength`
119+
* or maxlength attributes.
120+
* @param {string=} aria-label Aria-label is required when no label is present. A warning message
121+
* 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
123+
* PRESENT. The placeholder text is copied to the aria-label attribute.
113124
* @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
114125
*
115126
* @usage
@@ -168,13 +179,13 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
168179
var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
169180
var isReadonly = angular.isDefined(attr.readonly);
170181

171-
if ( !containerCtrl ) return;
182+
if (!containerCtrl) return;
172183
if (containerCtrl.input) {
173184
throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
174185
}
175186
containerCtrl.input = element;
176187

177-
if(!containerCtrl.label) {
188+
if (!containerCtrl.label) {
178189
$mdAria.expect(element, 'aria-label', element.attr('placeholder'));
179190
}
180191

@@ -195,8 +206,8 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
195206
}
196207

197208
var isErrorGetter = containerCtrl.isErrorGetter || function() {
198-
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
199-
};
209+
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
210+
};
200211
scope.$watch(isErrorGetter, containerCtrl.setInvalid);
201212

202213
ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
@@ -232,14 +243,15 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
232243
containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
233244
return arg;
234245
}
246+
235247
function inputCheckValue() {
236248
// An input's value counts if its length > 0,
237249
// or if the input's validity state says it has bad input (eg string in a number input)
238-
containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity||{}).badInput);
250+
containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
239251
}
240252

241253
function setupTextarea() {
242-
if(angular.isDefined(element.attr('md-no-autogrow'))) {
254+
if (angular.isDefined(element.attr('md-no-autogrow'))) {
243255
return;
244256
}
245257

@@ -250,7 +262,7 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
250262
var lineHeight = null;
251263
// can't check if height was or not explicity set,
252264
// so rows attribute will take precedence if present
253-
if(node.hasAttribute('rows')) {
265+
if (node.hasAttribute('rows')) {
254266
min_rows = parseInt(node.getAttribute('rows'));
255267
}
256268

@@ -269,7 +281,7 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
269281
}
270282
element.on('keydown input', onChangeTextarea);
271283

272-
if(isNaN(min_rows)) {
284+
if (isNaN(min_rows)) {
273285
element.attr('rows', '1');
274286

275287
element.on('scroll', onScroll);
@@ -288,15 +300,15 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
288300
// temporarily disables element's flex so its height 'runs free'
289301
element.addClass('md-no-flex');
290302

291-
if(isNaN(min_rows)) {
303+
if (isNaN(min_rows)) {
292304
node.style.height = "auto";
293305
node.scrollTop = 0;
294306
var height = getHeight();
295307
if (height) node.style.height = height + 'px';
296308
} else {
297309
node.setAttribute("rows", 1);
298310

299-
if(!lineHeight) {
311+
if (!lineHeight) {
300312
node.style.minHeight = '0';
301313

302314
lineHeight = element.prop('clientHeight');
@@ -313,7 +325,7 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
313325
container.style.height = 'auto';
314326
}
315327

316-
function getHeight () {
328+
function getHeight() {
317329
var line = node.scrollHeight - node.offsetHeight;
318330
return node.offsetHeight + (line > 0 ? line : 0);
319331
}
@@ -358,7 +370,7 @@ function mdMaxlengthDirective($animate) {
358370
if (angular.isNumber(value) && value > 0) {
359371
if (!charCountEl.parent().length) {
360372
$animate.enter(charCountEl, containerCtrl.element,
361-
angular.element(containerCtrl.element[0].lastElementChild));
373+
angular.element(containerCtrl.element[0].lastElementChild));
362374
}
363375
renderCharCount();
364376
} else {
@@ -374,7 +386,7 @@ function mdMaxlengthDirective($animate) {
374386
};
375387

376388
function renderCharCount(value) {
377-
charCountEl.text( ( element.val() || value || '' ).length + '/' + maxlength );
389+
charCountEl.text(( element.val() || value || '' ).length + '/' + maxlength);
378390
return value;
379391
}
380392
}
@@ -389,22 +401,28 @@ function placeholderDirective($log) {
389401
};
390402

391403
function postLink(scope, element, attr, inputContainer) {
404+
// If there is no input container, just return
392405
if (!inputContainer) return;
393-
if (angular.isDefined(inputContainer.element.attr('md-no-float'))) return;
394406

407+
// Add a placeholder class so we can target it in the CSS
408+
inputContainer.setHasPlaceholder(true);
409+
410+
var label = inputContainer.element.find('label');
411+
var hasNoFloat = angular.isDefined(inputContainer.element.attr('md-no-float'));
412+
413+
// If we have a label, or they specify the md-no-float attribute, just return
414+
if ((label && label.length) || hasNoFloat) return;
415+
416+
// Otherwise, grab/remove the placeholder
395417
var placeholderText = attr.placeholder;
396418
element.removeAttr('placeholder');
397419

398-
if ( inputContainer.element.find('label').length == 0 ) {
399-
if (inputContainer.input && inputContainer.input[0].nodeName != 'MD-SELECT') {
400-
var placeholder = '<label ng-click="delegateClick()">' + placeholderText + '</label>';
420+
// And add the placeholder text as a separate label
421+
if (inputContainer.input && inputContainer.input[0].nodeName != 'MD-SELECT') {
422+
var placeholder = '<label ng-click="delegateClick()">' + placeholderText + '</label>';
401423

402-
inputContainer.element.addClass('md-icon-float');
403-
inputContainer.element.prepend(placeholder);
404-
}
405-
} else if (element[0].nodeName != 'MD-SELECT') {
406-
$log.warn("The placeholder='" + placeholderText + "' will be ignored since this md-input-container has a child label element.");
424+
inputContainer.element.addClass('md-icon-float');
425+
inputContainer.element.prepend(placeholder);
407426
}
408-
409427
}
410428
}

src/components/input/input.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,21 @@ md-input-container {
197197
}
198198

199199
&.md-input-focused,
200+
&.md-input-has-placeholder,
200201
&.md-input-has-value {
201-
label:not(.md-no-float) {
202+
label:not(.md-no-float) {
202203
transform: translate3d(0,$input-label-float-offset,0) scale($input-label-float-scale);
203204
}
204205
}
205206

207+
// If we have an existing value; don't animate the transform as it happens on page load and
208+
// causes erratic/unnecessary animation
209+
&.md-input-has-value {
210+
label {
211+
transition: none;
212+
}
213+
}
214+
206215
// Use wide border in error state or in focused state
207216
&.md-input-focused .md-input,
208217
.md-input.ng-invalid.ng-dirty {

src/components/input/input.spec.js

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ describe('md-input-container directive', function() {
77
var container;
88
inject(function($rootScope, $compile) {
99
container = $compile((isForm ? '<form>' : '') +
10-
'<md-input-container><input ' +(attrs||'')+ '><label></label></md-input-container>' +
11-
(isForm ? '<form>' : ''))($rootScope);
10+
'<md-input-container><input ' + (attrs || '') + '><label></label></md-input-container>' +
11+
(isForm ? '<form>' : ''))($rootScope);
1212
$rootScope.$apply();
1313
});
1414
return container;
@@ -107,10 +107,10 @@ describe('md-input-container directive', function() {
107107

108108
it('should work with a constant', inject(function($rootScope, $compile) {
109109
var el = $compile('<form name="form">' +
110-
' <md-input-container>' +
111-
' <input md-maxlength="5" ng-model="foo" name="foo">' +
112-
' </md-input-container>' +
113-
'</form>')($rootScope);
110+
' <md-input-container>' +
111+
' <input md-maxlength="5" ng-model="foo" name="foo">' +
112+
' </md-input-container>' +
113+
'</form>')($rootScope);
114114
$rootScope.$apply();
115115
expect($rootScope.form.foo.$error['md-maxlength']).toBeFalsy();
116116
expect(getCharCounter(el).text()).toBe('0/5');
@@ -132,10 +132,10 @@ describe('md-input-container directive', function() {
132132

133133
it('should add and remove maxlength element & error with expression', inject(function($rootScope, $compile) {
134134
var el = $compile('<form name="form">' +
135-
' <md-input-container>' +
136-
' <input md-maxlength="max" ng-model="foo" name="foo">' +
137-
' </md-input-container>' +
138-
'</form>')($rootScope);
135+
' <md-input-container>' +
136+
' <input md-maxlength="max" ng-model="foo" name="foo">' +
137+
' </md-input-container>' +
138+
'</form>')($rootScope);
139139

140140
$rootScope.$apply();
141141
expect($rootScope.form.foo.$error['md-maxlength']).toBeFalsy();
@@ -165,21 +165,26 @@ describe('md-input-container directive', function() {
165165
}));
166166

167167
it('should ignore placeholder when a label element is present', inject(function($rootScope, $compile) {
168-
var el = $compile('<md-input-container><label>Hello</label><input ng-model="foo" placeholder="some placeholder"></md-input-container>')($rootScope);
169-
var placeholder = el[0].querySelector('.md-placeholder');
170-
var label = el.find('label')[0];
168+
var el = $compile(
169+
'<md-input-container>' +
170+
' <label>Hello</label>' +
171+
' <input ng-model="foo" placeholder="some placeholder" />' +
172+
'</md-input-container>'
173+
)($rootScope);
171174

172-
expect(el.find('input')[0].hasAttribute('placeholder')).toBe(false);
173-
expect(label).toBeTruthy();
174-
expect(label.textContent).toEqual('Hello');
175-
}));
175+
var label = el.find('label')[0];
176+
177+
expect(el.find('input')[0].hasAttribute('placeholder')).toBe(true);
178+
expect(label).toBeTruthy();
179+
expect(label.textContent).toEqual('Hello');
180+
}));
176181

177182
it('should put an aria-label on the input when no label is present', inject(function($rootScope, $compile) {
178183
var el = $compile('<form name="form">' +
179-
' <md-input-container md-no-float>' +
180-
' <input placeholder="baz" md-maxlength="max" ng-model="foo" name="foo">' +
181-
' </md-input-container>' +
182-
'</form>')($rootScope);
184+
' <md-input-container md-no-float>' +
185+
' <input placeholder="baz" md-maxlength="max" ng-model="foo" name="foo">' +
186+
' </md-input-container>' +
187+
'</form>')($rootScope);
183188

184189
$rootScope.$apply();
185190

@@ -190,10 +195,10 @@ describe('md-input-container directive', function() {
190195
it('should put the container in "has value" state when input has a static value', inject(function($rootScope, $compile) {
191196
var scope = $rootScope.$new();
192197
var template =
193-
'<md-input-container>' +
194-
'<label>Name</label>' +
195-
'<input value="Larry">' +
196-
'</md-input-container>';
198+
'<md-input-container>' +
199+
'<label>Name</label>' +
200+
'<input value="Larry">' +
201+
'</md-input-container>';
197202

198203
var element = $compile(template)(scope);
199204
scope.$apply();

0 commit comments

Comments
 (0)