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

Commit b5dd150

Browse files
topherfangioThomasBurleson
authored andcommitted
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. Closes #4623.
1 parent bfb8dac commit b5dd150

File tree

4 files changed

+87
-55
lines changed

4 files changed

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

@@ -73,6 +76,9 @@ function mdInputContainerDirective($mdTheming, $parse) {
7376
self.setHasMessages = function(hasMessages) {
7477
$element.toggleClass('md-input-has-messages', !!hasMessages);
7578
};
79+
self.setHasPlaceholder = function(hasPlaceholder) {
80+
$element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
81+
};
7682
self.setInvalid = function(isInvalid) {
7783
$element.toggleClass('md-input-invalid', !!isInvalid);
7884
};
@@ -110,10 +116,15 @@ function labelDirective() {
110116
* @description
111117
* Use the `<input>` or the `<textarea>` as a child of an `<md-input-container>`.
112118
*
113-
* @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/>
114-
* 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.
115-
* @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.
116-
* @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.
119+
* @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is
120+
* specified, a character counter will be shown underneath the input.<br/><br/>
121+
* The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't
122+
* want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength`
123+
* or maxlength attributes.
124+
* @param {string=} aria-label Aria-label is required when no label is present. A warning message
125+
* will be logged in the console if not present.
126+
* @param {string=} placeholder An alternative approach to using aria-label when the label is not
127+
* PRESENT. The placeholder text is copied to the aria-label attribute.
117128
* @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
118129
*
119130
* @usage
@@ -172,13 +183,13 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
172183
var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
173184
var isReadonly = angular.isDefined(attr.readonly);
174185

175-
if ( !containerCtrl ) return;
186+
if (!containerCtrl) return;
176187
if (containerCtrl.input) {
177188
throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
178189
}
179190
containerCtrl.input = element;
180191

181-
if(!containerCtrl.label) {
192+
if (!containerCtrl.label) {
182193
$mdAria.expect(element, 'aria-label', element.attr('placeholder'));
183194
}
184195

@@ -199,8 +210,8 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
199210
}
200211

201212
var isErrorGetter = containerCtrl.isErrorGetter || function() {
202-
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
203-
};
213+
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
214+
};
204215
scope.$watch(isErrorGetter, containerCtrl.setInvalid);
205216

206217
ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
@@ -236,14 +247,15 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
236247
containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
237248
return arg;
238249
}
250+
239251
function inputCheckValue() {
240252
// An input's value counts if its length > 0,
241253
// or if the input's validity state says it has bad input (eg string in a number input)
242-
containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity||{}).badInput);
254+
containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
243255
}
244256

245257
function setupTextarea() {
246-
if(angular.isDefined(element.attr('md-no-autogrow'))) {
258+
if (angular.isDefined(element.attr('md-no-autogrow'))) {
247259
return;
248260
}
249261

@@ -254,7 +266,7 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
254266
var lineHeight = null;
255267
// can't check if height was or not explicity set,
256268
// so rows attribute will take precedence if present
257-
if(node.hasAttribute('rows')) {
269+
if (node.hasAttribute('rows')) {
258270
min_rows = parseInt(node.getAttribute('rows'));
259271
}
260272

@@ -273,7 +285,7 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
273285
}
274286
element.on('keydown input', onChangeTextarea);
275287

276-
if(isNaN(min_rows)) {
288+
if (isNaN(min_rows)) {
277289
element.attr('rows', '1');
278290

279291
element.on('scroll', onScroll);
@@ -292,15 +304,15 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
292304
// temporarily disables element's flex so its height 'runs free'
293305
element.addClass('md-no-flex');
294306

295-
if(isNaN(min_rows)) {
307+
if (isNaN(min_rows)) {
296308
node.style.height = "auto";
297309
node.scrollTop = 0;
298310
var height = getHeight();
299311
if (height) node.style.height = height + 'px';
300312
} else {
301313
node.setAttribute("rows", 1);
302314

303-
if(!lineHeight) {
315+
if (!lineHeight) {
304316
node.style.minHeight = '0';
305317

306318
lineHeight = element.prop('clientHeight');
@@ -317,7 +329,7 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
317329
container.style.height = 'auto';
318330
}
319331

320-
function getHeight () {
332+
function getHeight() {
321333
var line = node.scrollHeight - node.offsetHeight;
322334
return node.offsetHeight + (line > 0 ? line : 0);
323335
}
@@ -378,7 +390,7 @@ function mdMaxlengthDirective($animate) {
378390
};
379391

380392
function renderCharCount(value) {
381-
charCountEl.text( ( element.val() || value || '' ).length + '/' + maxlength );
393+
charCountEl.text(( element.val() || value || '' ).length + '/' + maxlength);
382394
return value;
383395
}
384396
}
@@ -393,23 +405,29 @@ function placeholderDirective($log) {
393405
};
394406

395407
function postLink(scope, element, attr, inputContainer) {
408+
// If there is no input container, just return
396409
if (!inputContainer) return;
397-
if (angular.isDefined(inputContainer.element.attr('md-no-float'))) return;
398410

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

402-
if ( inputContainer.element.find('label').length == 0 ) {
403-
if (inputContainer.input && inputContainer.input[0].nodeName != 'MD-SELECT') {
404-
var placeholder = '<label ng-click="delegateClick()">' + placeholderText + '</label>';
424+
// And add the placeholder text as a separate label
425+
if (inputContainer.input && inputContainer.input[0].nodeName != 'MD-SELECT') {
426+
var placeholder = '<label ng-click="delegateClick()">' + placeholderText + '</label>';
405427

406-
inputContainer.element.addClass('md-icon-float');
407-
inputContainer.element.prepend(placeholder);
408-
}
409-
} else if (element[0].nodeName != 'MD-SELECT') {
410-
$log.warn("The placeholder='" + placeholderText + "' will be ignored since this md-input-container has a child label element.");
428+
inputContainer.element.addClass('md-icon-float');
429+
inputContainer.element.prepend(placeholder);
411430
}
412-
413431
}
414432
}
415433

src/components/input/input.scss

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

229229
&.md-input-focused,
230+
&.md-input-has-placeholder,
230231
&.md-input-has-value {
231-
label:not(.md-no-float) {
232+
label:not(.md-no-float) {
232233
transform: translate3d(0,$input-label-float-offset,0) scale($input-label-float-scale);
233234
}
234235
}
235236

237+
// If we have an existing value; don't animate the transform as it happens on page load and
238+
// causes erratic/unnecessary animation
239+
&.md-input-has-value {
240+
label {
241+
transition: none;
242+
}
243+
}
244+
236245
// Use wide border in error state or in focused state
237246
&.md-input-focused .md-input,
238247
.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)