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

Commit ac9e4f6

Browse files
committed
fix(input): ngMessages jump with ngShow
WIP BRANCH - Not ready for merge. When the ngMessages directive was used with the ngShow directive inside of an md-input-container, the UI would jump rather than properly animating. Additionally, if the messages spanned multiple lines, the animations were incorrect due to hard-coded values. Fix many styles and move animations into JS so we can properly calculate message heights. _NOTE: The messages may still jump due to angular/angular.js#12969; this fixes a different, but similar bug._
1 parent 4b24259 commit ac9e4f6

File tree

5 files changed

+266
-49
lines changed

5 files changed

+266
-49
lines changed

src/components/input/demoErrors/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<md-content layout-padding>
44
<form name="projectForm">
5+
56
<md-input-container class="md-block">
67
<label>Description</label>
78
<input md-maxlength="30" required name="description" ng-model="project.description">

src/components/input/input.js

+190-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ angular.module('material.components.input', [
1212
.directive('textarea', inputTextareaDirective)
1313
.directive('mdMaxlength', mdMaxlengthDirective)
1414
.directive('placeholder', placeholderDirective)
15-
.directive('ngMessages', ngMessagesDirective);
15+
.directive('ngMessages', ngMessagesDirective)
16+
.directive('ngMessage', ngMessageDirective)
17+
.directive('ngMessageExp', ngMessageDirective)
18+
19+
.animation('.md-input-invalid', mdInputInvalidMessagesAnimation)
20+
.animation('.md-input-messages-animation', ngMessagesAnimation)
21+
.animation('.md-input-message-animation', ngMessageAnimation);
1622

1723
/**
1824
* @ngdoc directive
@@ -59,7 +65,7 @@ function mdInputContainerDirective($mdTheming, $parse) {
5965
if (element.find('md-icon').length) element.addClass('md-has-icon');
6066
}
6167

62-
function ContainerCtrl($scope, $element, $attrs) {
68+
function ContainerCtrl($scope, $element, $attrs, $animate) {
6369
var self = this;
6470

6571
self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
@@ -81,7 +87,11 @@ function mdInputContainerDirective($mdTheming, $parse) {
8187
$element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
8288
};
8389
self.setInvalid = function(isInvalid) {
84-
$element.toggleClass('md-input-invalid', !!isInvalid);
90+
if (isInvalid) {
91+
$animate.addClass($element, 'md-input-invalid');
92+
} else {
93+
$animate.removeClass($element, 'md-input-invalid');
94+
}
8595
};
8696
$scope.$watch(function() {
8797
return self.label && self.input;
@@ -490,10 +500,13 @@ function ngMessagesDirective() {
490500
require: '^^?mdInputContainer'
491501
};
492502

493-
function postLink(scope, element, attr, inputContainer) {
503+
function postLink(scope, element, attrs, inputContainer) {
494504
// If we are not a child of an input container, don't do anything
495505
if (!inputContainer) return;
496506

507+
// Add our animation class
508+
element.toggleClass('md-input-messages-animation', true);
509+
497510
// Tell our parent input container we have messages so we can set the proper classes
498511
inputContainer.setHasMessages(true);
499512

@@ -503,3 +516,176 @@ function ngMessagesDirective() {
503516
});
504517
}
505518
}
519+
520+
function ngMessageDirective($mdUtil) {
521+
return {
522+
restrict: 'EA',
523+
compile: compile,
524+
priority: 100
525+
};
526+
527+
function compile(element) {
528+
var inputContainer = $mdUtil.getClosest(element, "md-input-container");
529+
530+
// If we are not a child of an input container, don't do anything
531+
if (!inputContainer) return;
532+
533+
// Add our animation class
534+
element.toggleClass('md-input-message-animation', true);
535+
536+
return {};
537+
}
538+
}
539+
540+
function mdInputInvalidMessagesAnimation($q, $animateCss) {
541+
return {
542+
addClass: function(element, className, done) {
543+
console.log(' *** addClass: ', className);
544+
if (className == "md-input-invalid" && shouldShowMessages(element)) {
545+
showInputMessages(element, $animateCss, $q).finally(done);
546+
}
547+
}
548+
// NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
549+
550+
/*
551+
removeClass: function(element, className, done) {
552+
console.log(' *** removeClass: ', className);
553+
if (className == "md-input-invalid") {
554+
hideInputMessages(element, $animateCss, $q).finally(done);
555+
}
556+
}
557+
*/
558+
}
559+
}
560+
561+
function ngMessagesAnimation($q, $animateCss) {
562+
return {
563+
addClass: function(element, className, done) {
564+
if (className == "ng-hide") {
565+
console.log(' *** messages hide');
566+
hideInputMessages(element, $animateCss, $q).finally(done);
567+
} else {
568+
done();
569+
}
570+
},
571+
572+
removeClass: function(element, className, done) {
573+
if (className == "ng-hide" && shouldShowMessages(element)) {
574+
console.log(' *** messages show');
575+
showInputMessages(element, $animateCss, $q).finally(done);
576+
} else {
577+
done();
578+
}
579+
}
580+
}
581+
}
582+
583+
function ngMessageAnimation($animateCss) {
584+
return {
585+
enter: function(element, done) {
586+
console.log(' *** message enter');
587+
if (shouldShowMessages(element)) {
588+
return showMessage(element, $animateCss);
589+
} else {
590+
done();
591+
}
592+
},
593+
594+
leave: function(element) {
595+
console.log(' *** message leave');
596+
return hideMessage(element, $animateCss);
597+
}
598+
}
599+
}
600+
601+
function showInputMessages(element, $animateCss, $q) {
602+
var animators = [], animator;
603+
var messages = getMessagesElement(element);
604+
605+
angular.forEach(messages.children(), function(child) {
606+
if (!child.classList.contains("md-char-counter")) {
607+
animator = showMessage(angular.element(child), $animateCss);
608+
609+
animators.push(animator.start());
610+
}
611+
});
612+
613+
return $q.all(animators);
614+
}
615+
616+
function hideInputMessages(element, $animateCss, $q) {
617+
var animators = [], animator;
618+
var messages = getMessagesElement(element);
619+
620+
angular.forEach(messages.children(), function(child) {
621+
if (!child.classList.contains("md-char-counter")) {
622+
animator = showMessage(angular.element(child), $animateCss);
623+
624+
animators.push(animator.start());
625+
}
626+
});
627+
628+
return $q.all(animators);
629+
}
630+
631+
function showMessage(element, $animateCss) {
632+
var height = element[0].offsetHeight;
633+
var styles = window.getComputedStyle(element[0]);
634+
635+
return $animateCss(element, {
636+
from: {"opacity": styles.opacity, "margin-top": -height + "px"},
637+
to: {"opacity": 1, "margin-top": "0"},
638+
duration: 0.3
639+
});
640+
}
641+
642+
function hideMessage(element, $animateCss) {
643+
var height = element[0].offsetHeight;
644+
var styles = window.getComputedStyle(element[0]);
645+
646+
return $animateCss(element, {
647+
from: {"opacity": styles.opacity, "margin-top": 0},
648+
to: {"opacity": 0, "margin-top": -height + "px"},
649+
duration: 0.3
650+
});
651+
}
652+
653+
function shouldShowMessages(element) {
654+
var input = getInputElement(element);
655+
656+
var shouldShow = (
657+
// If we are invalid
658+
input.hasClass('md-input-invalid')
659+
660+
// or the user tells us not to auto-hide
661+
|| input.hasClass('md-no-auto-hide-messages')
662+
663+
// or we see a known show/hide/switch directive
664+
|| hasVisibilityDirective(input)
665+
);
666+
667+
console.log(' *** shouldShow: ', shouldShow);
668+
669+
return shouldShow;
670+
}
671+
672+
function getInputElement(element) {
673+
var inputContainer = element.controller('mdInputContainer');
674+
675+
return inputContainer.element;
676+
}
677+
678+
function getMessagesElement(element) {
679+
var input = getInputElement(element);
680+
var selector = 'ng-messages,data-ng-messages,x-ng-messages,' +
681+
'[ng-messages],[data-ng-messages],[x-ng-messages]';
682+
683+
console.log(' *** getting messages: ', element, input, input[0].querySelector(selector));
684+
685+
return angular.element(input[0].querySelector(selector));
686+
}
687+
688+
function hasVisibilityDirective(input) {
689+
// TODO add functionality to grab the child messages
690+
return false;
691+
}

0 commit comments

Comments
 (0)