Skip to content

Commit

Permalink
fix(mdInput): Support multiple ng-messages simultaneously.
Browse files Browse the repository at this point in the history
Previously, multiple `ng-message`s would render on top of each
other. Fix by altering CSS position and altering transition to
support multiple messages (i.e. potentially varying height).

Also some other small fixes to inputs/errors:

 * Fix number input widths in Firefox
 * Update errors demo messages to be more dynamic and show multiple errors
 * Update SCSS to allow `ng-message-exp` and associated `data-` and `x-` attributes
 * Add demo using `ng-message-exp` to show new SCSS styles being applied

Fixes angular#2648. Fixes angular#1957. Fixes angular#1793. Closes angular#4647. Closes angular#4472. Closes angular#4008.

> Should also close PRs angular#4472 and angular#4008. Thanks to @bopm and @iksose for the
initial PRs!
  • Loading branch information
topherfangio authored and kennethcachia committed Sep 23, 2015
1 parent b03935a commit 9d74ade
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 26 deletions.
39 changes: 34 additions & 5 deletions src/components/input/demoErrors/index.html
Expand Up @@ -25,13 +25,42 @@ <h1 class="md-toolbar-tools">
</div>
</md-input-container>

<md-input-container>
<label>Client Email</label>
<input required type="email" name="clientEmail" ng-model="project.clientEmail"
minlength="10" maxlength="100" ng-pattern="/^.+@.+\..+$/" />

<div ng-messages="projectForm.clientEmail.$error" role="alert">
<div ng-message-exp="['required', 'minlength', 'maxlength', 'pattern']">
Your email must be between 10 and 100 characters long and look like an e-mail address.
</div>
</div>
</md-input-container>

<md-input-container>
<label>Hourly Rate (USD)</label>
<input required type="number" step="any" name="rate" ng-model="project.rate" min="800" max="4999">
<div ng-messages="projectForm.rate.$error">
<div ng-message="required">You've got to charge something! You can't just <b>give away</b> a Missile Defense System.</div>
<div ng-message="min">You should charge at least $800 an hour. This job is a big deal... if you mess up, everyone dies!</div>
<div ng-message="max">$5,000 an hour? That's a little ridiculous. I doubt event Bill Clinton could afford that.</div>
<input required type="number" step="any" name="rate" ng-model="project.rate" min="800"
max="4999" ng-pattern="/^1234$/">

<div ng-messages="projectForm.rate.$error" multiple>
<div ng-message="required">
You've got to charge something! You can't just <b>give away</b> a Missile Defense
System.
</div>

<div ng-message="min">
You should charge at least $800 an hour. This job is a big deal... if you mess up,
everyone dies!
</div>

<div ng-message="pattern">
You should charge exactly $1,234.
</div>

<div ng-message="max">
{{projectForm.rate.$viewValue | currency:"$":0}} an hour? That's a little ridiculous. I
doubt event Bill Clinton could afford that.
</div>
</div>
</md-input-container>
</form>
Expand Down
10 changes: 6 additions & 4 deletions src/components/input/input-theme.scss
Expand Up @@ -16,12 +16,13 @@ md-input-container.md-THEME_NAME-theme {
color: '{{foreground-3}}';
}

ng-messages,
[ng-message], [data-ng-message], [x-ng-message] {
color: '{{warn-500}}'
ng-messages, [ng-messages],
ng-message, data-ng-message, x-ng-message,
[ng-message], [data-ng-message], [x-ng-message],
[ng-message-exp], [data-ng-message-exp], [x-ng-message-exp] {
color: '{{warn-500}}';
}


&:not(.md-input-invalid) {
&.md-input-has-value {
label {
Expand Down Expand Up @@ -65,6 +66,7 @@ md-input-container.md-THEME_NAME-theme {
}
ng-message, data-ng-message, x-ng-message,
[ng-message], [data-ng-message], [x-ng-message],
[ng-message-exp], [data-ng-message-exp], [x-ng-message-exp],
.md-char-counter {
color: '{{warn-500}}';
}
Expand Down
36 changes: 32 additions & 4 deletions src/components/input/input.js
Expand Up @@ -11,7 +11,8 @@ angular.module('material.components.input', [
.directive('input', inputTextareaDirective)
.directive('textarea', inputTextareaDirective)
.directive('mdMaxlength', mdMaxlengthDirective)
.directive('placeholder', placeholderDirective);
.directive('placeholder', placeholderDirective)
.directive('ngMessages', ngMessagesDirective);

/**
* @ngdoc directive
Expand Down Expand Up @@ -69,6 +70,9 @@ function mdInputContainerDirective($mdTheming, $parse) {
self.setHasValue = function(hasValue) {
$element.toggleClass('md-input-has-value', !!hasValue);
};
self.setHasMessages = function(hasMessages) {
$element.toggleClass('md-input-has-messages', !!hasMessages);
};
self.setInvalid = function(isInvalid) {
$element.toggleClass('md-input-invalid', !!isInvalid);
};
Expand Down Expand Up @@ -341,11 +345,12 @@ function mdMaxlengthDirective($animate) {
var ngModelCtrl = ctrls[0];
var containerCtrl = ctrls[1];
var charCountEl = angular.element('<div class="md-char-counter">');
var input = angular.element(containerCtrl.element[0].querySelector('[md-maxlength]'));

// Stop model from trimming. This makes it so whitespace
// over the maxlength still counts as invalid.
attr.$set('ngTrim', 'false');
containerCtrl.element.append(charCountEl);
input.after(charCountEl);

ngModelCtrl.$formatters.push(renderCharCount);
ngModelCtrl.$viewChangeListeners.push(renderCharCount);
Expand All @@ -357,8 +362,7 @@ function mdMaxlengthDirective($animate) {
maxlength = value;
if (angular.isNumber(value) && value > 0) {
if (!charCountEl.parent().length) {
$animate.enter(charCountEl, containerCtrl.element,
angular.element(containerCtrl.element[0].lastElementChild));
$animate.enter(charCountEl, containerCtrl.element, input);
}
renderCharCount();
} else {
Expand Down Expand Up @@ -408,3 +412,27 @@ function placeholderDirective($log) {

}
}

function ngMessagesDirective() {
return {
restrict: 'EA',
link: postLink,

// This is optional because we don't want target *all* ngMessage instances, just those inside of
// mdInputContainer.
require: '^^?mdInputContainer'
};

function postLink(scope, element, attr, inputContainer) {
// If we are not a child of an input container, don't do anything
if (!inputContainer) return;

// Tell our parent input container we have messages so we can set the proper classes
inputContainer.setHasMessages(true);

// When destroyed, inform our input container
scope.$on('$destroy', function() {
inputContainer.setHasMessages(false);
});
}
}
56 changes: 43 additions & 13 deletions src/components/input/input.scss
Expand Up @@ -28,6 +28,12 @@ md-input-container {
padding: $input-container-padding;
padding-bottom: $input-container-padding + $input-error-height;

// When we have ng-messages, remove the input error height from our bottom padding, since the
// ng-messages wrapper has a min-height of 1 error (so we don't adjust height as often; see below)
&.md-input-has-messages {
padding-bottom: $input-container-padding;
}

> md-icon {
position: absolute;
top: 5px;
Expand Down Expand Up @@ -143,6 +149,9 @@ md-input-container {
-ms-flex-preferred-size: $input-line-height; //IE fix
border-radius: 0;

// Fix number inputs in Firefox to be full-width
width: auto;

&:focus {
outline: none;
}
Expand All @@ -156,45 +165,66 @@ md-input-container {
}
}

.md-char-counter {
position: absolute;
right: 0;
order: 3;
}

ng-messages, data-ng-messages, x-ng-messages,
[ng-messages], [data-ng-messages], [x-ng-messages] {
order: 3;
position: relative;
order: 4;
min-height: $input-error-height;
}

ng-message, data-ng-message, x-ng-message,
[ng-message], [data-ng-message], [x-ng-message],
[ng-message-exp], [data-ng-message-exp], [x-ng-message-exp],
.md-char-counter {
$input-error-line-height: $input-error-font-size + 2px;
//-webkit-font-smoothing: antialiased;
position: absolute;
font-size: $input-error-font-size;
line-height: $input-error-height;
line-height: $input-error-line-height;
overflow: hidden;

// Add some top padding which is equal to half the difference between the expected height
// and the actual height
$error-padding-top: ($input-error-height - $input-error-line-height) / 2;
padding-top: $error-padding-top;

&:not(.md-char-counter) {
padding-right: rem(3);
padding-right: rem(5);
}

&.ng-enter {
transition: $swift-ease-out;
transition-delay: 0.2s;
transition: $swift-ease-in;

// Delay the enter transition so it happens after the leave
transition-delay: $swift-ease-in-duration / 1.5;

// Since we're delaying the transition, we speed up the duration a little bit to compensate
transition-duration: $swift-ease-in-duration / 1.5;
}
&.ng-leave {
transition: $swift-ease-in;
transition: $swift-ease-out;

// Speed up the duration (see enter comment above)
transition-duration: $swift-ease-out-duration / 1.5;
}
&.ng-enter,
&.ng-leave.ng-leave-active {
// Move the error upwards off the screen and fade it out
margin-top: -$input-error-line-height - $error-padding-top;
opacity: 0;
transform: translate3d(0, -20%, 0);
}
&.ng-leave,
&.ng-enter.ng-enter-active {
// Move the error down into position and fade it in
margin-top: 0;
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.md-char-counter {
bottom: $input-container-padding;
right: $input-container-padding;
}

&.md-input-focused,
&.md-input-has-value {
Expand Down

0 comments on commit 9d74ade

Please sign in to comment.