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

feat(mdInput): Add support for both labels and placeholders. #4623

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses the placeholder directive. But what uses/inserts the md-placeholder style?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The styling is added dynamically based on whether they include both the label and the placeholder which are standard attributes. I think the idea was to make the inputs as close to a standard HTML input as possible.

It's all done/decided in the bottom of input.js in the function placeholderDirective() and documented in the input docs.

Is this what you were asking?

</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