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

Commit

Permalink
feat(mdInput): Add support for both labels and placeholders.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
topherfangio authored and ThomasBurleson committed Sep 17, 2015
1 parent bfb8dac commit b5dd150
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 55 deletions.
2 changes: 1 addition & 1 deletion src/components/input/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
</md-input-container>
<md-input-container flex>
<label>Postal Code</label>
<input ng-model="user.postalCode">
<input ng-model="user.postalCode" placeholder="12345">
</md-input-container>
</div>

Expand Down
74 changes: 46 additions & 28 deletions src/components/input/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ angular.module('material.components.input', [
* Input and textarea elements will not behave properly unless the md-input-container
* parent is provided.
*
* @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.
* @param md-no-float {boolean=} When present, placeholders will not be converted to floating labels
* @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.
* @param md-no-float {boolean=} When present, placeholders will not be converted to floating
* labels.
*
* @usage
* <hljs lang="html">
Expand All @@ -55,6 +57,7 @@ function mdInputContainerDirective($mdTheming, $parse) {
function postLink(scope, element, attr) {
$mdTheming(element);
}

function ContainerCtrl($scope, $element, $attrs) {
var self = this;

Expand All @@ -73,6 +76,9 @@ function mdInputContainerDirective($mdTheming, $parse) {
self.setHasMessages = function(hasMessages) {
$element.toggleClass('md-input-has-messages', !!hasMessages);
};
self.setHasPlaceholder = function(hasPlaceholder) {
$element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
};
self.setInvalid = function(isInvalid) {
$element.toggleClass('md-input-invalid', !!isInvalid);
};
Expand Down Expand Up @@ -110,10 +116,15 @@ function labelDirective() {
* @description
* Use the `<input>` or the `<textarea>` as a child of an `<md-input-container>`.
*
* @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/>
* 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.
* @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.
* @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.
* @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/>
* 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.
* @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.
* @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.
* @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
*
* @usage
Expand Down Expand Up @@ -172,13 +183,13 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
var isReadonly = angular.isDefined(attr.readonly);

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

if(!containerCtrl.label) {
if (!containerCtrl.label) {
$mdAria.expect(element, 'aria-label', element.attr('placeholder'));
}

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

var isErrorGetter = containerCtrl.isErrorGetter || function() {
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
};
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
};
scope.$watch(isErrorGetter, containerCtrl.setInvalid);

ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
Expand Down Expand Up @@ -236,14 +247,15 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
return arg;
}

function inputCheckValue() {
// An input's value counts if its length > 0,
// or if the input's validity state says it has bad input (eg string in a number input)
containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity||{}).badInput);
containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
}

function setupTextarea() {
if(angular.isDefined(element.attr('md-no-autogrow'))) {
if (angular.isDefined(element.attr('md-no-autogrow'))) {
return;
}

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

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

if(isNaN(min_rows)) {
if (isNaN(min_rows)) {
element.attr('rows', '1');

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

if(isNaN(min_rows)) {
if (isNaN(min_rows)) {
node.style.height = "auto";
node.scrollTop = 0;
var height = getHeight();
if (height) node.style.height = height + 'px';
} else {
node.setAttribute("rows", 1);

if(!lineHeight) {
if (!lineHeight) {
node.style.minHeight = '0';

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

function getHeight () {
function getHeight() {
var line = node.scrollHeight - node.offsetHeight;
return node.offsetHeight + (line > 0 ? line : 0);
}
Expand Down Expand Up @@ -378,7 +390,7 @@ function mdMaxlengthDirective($animate) {
};

function renderCharCount(value) {
charCountEl.text( ( element.val() || value || '' ).length + '/' + maxlength );
charCountEl.text(( element.val() || value || '' ).length + '/' + maxlength);
return value;
}
}
Expand All @@ -393,23 +405,29 @@ function placeholderDirective($log) {
};

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

// Add a placeholder class so we can target it in the CSS
inputContainer.setHasPlaceholder(true);

var label = inputContainer.element.find('label');
var hasNoFloat = angular.isDefined(inputContainer.element.attr('md-no-float'));

// If we have a label, or they specify the md-no-float attribute, just return
if ((label && label.length) || hasNoFloat) return;

// Otherwise, grab/remove the placeholder
var placeholderText = attr.placeholder;
element.removeAttr('placeholder');

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

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

}
}

Expand Down
11 changes: 10 additions & 1 deletion src/components/input/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,21 @@ md-input-container {
}

&.md-input-focused,
&.md-input-has-placeholder,
&.md-input-has-value {
label:not(.md-no-float) {
label:not(.md-no-float) {
transform: translate3d(0,$input-label-float-offset,0) scale($input-label-float-scale);
}
}

// If we have an existing value; don't animate the transform as it happens on page load and
// causes erratic/unnecessary animation
&.md-input-has-value {
label {
transition: none;
}
}

// Use wide border in error state or in focused state
&.md-input-focused .md-input,
.md-input.ng-invalid.ng-dirty {
Expand Down
55 changes: 30 additions & 25 deletions src/components/input/input.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ describe('md-input-container directive', function() {
var container;
inject(function($rootScope, $compile) {
container = $compile((isForm ? '<form>' : '') +
'<md-input-container><input ' +(attrs||'')+ '><label></label></md-input-container>' +
(isForm ? '<form>' : ''))($rootScope);
'<md-input-container><input ' + (attrs || '') + '><label></label></md-input-container>' +
(isForm ? '<form>' : ''))($rootScope);
$rootScope.$apply();
});
return container;
Expand Down Expand Up @@ -107,10 +107,10 @@ describe('md-input-container directive', function() {

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

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

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

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

expect(el.find('input')[0].hasAttribute('placeholder')).toBe(false);
expect(label).toBeTruthy();
expect(label.textContent).toEqual('Hello');
}));
var label = el.find('label')[0];

expect(el.find('input')[0].hasAttribute('placeholder')).toBe(true);
expect(label).toBeTruthy();
expect(label.textContent).toEqual('Hello');
}));

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

$rootScope.$apply();

Expand All @@ -190,10 +195,10 @@ describe('md-input-container directive', function() {
it('should put the container in "has value" state when input has a static value', inject(function($rootScope, $compile) {
var scope = $rootScope.$new();
var template =
'<md-input-container>' +
'<label>Name</label>' +
'<input value="Larry">' +
'</md-input-container>';
'<md-input-container>' +
'<label>Name</label>' +
'<input value="Larry">' +
'</md-input-container>';

var element = $compile(template)(scope);
scope.$apply();
Expand Down

0 comments on commit b5dd150

Please sign in to comment.