ng-animate: only add to the dom after the animation delay - angularjs

I'm trying to use ng-animate with an ng-repeat (and ng-show) to fade out old content and replace it with new.
The problem I'm having is that during the remove and add animations, both the element(s) being added and the element(s) being removed have display:block.
I thought I could avoid this by using an animation-delay in the CSS, but that just delays the fade, and not the addition of the class that sets display on the element.
The result is a jerky transition.
This is my animation CSS (cut down):
.keyframe-fade.ng-enter,
.keyframe-fade.ng-move {
animation: 0.5s fade-in;
animation-delay: 1s;
animation-fill-mode: backwards;
}
.keyframe-fade.ng-leave {
animation: 0.5s fade-out;
}
But it's easier to demonstrate with this plunkr.
Any ideas?
NOTE: To be clear, the desired behaviour on the plunkr linked is that the coloured squares always take up the same space, i.e. they sit on the same line and the button doesn't move. If possible, I'd like to fix this without absolute positioning 'bodges' as the actual page I'm using this on is much more complex than the demo given.

The solution that I found for this is to augment the pure CSS animation with a very small amount of JavaScript.
To summarise the problem:
The entering element is added to the DOM with the ng-enter class at the same time that the leaving element is given the ng-leave class.
Though there is an animation delay, the entering element still takes up space
So this piece of javascript takes the element and adds ng-hide for the duration of the leave-animation, removing it afterwards.
.animation('.keyframe-fade', ['$timeout', function ($timeout){
return {
enter: function (element, done){
// Add ng-hide for the duration of the leave animation.
element.addClass('ng-hide');
$timeout(function(){
element.removeClass('ng-hide');
}, 500)
done();
}
}
}])
The duration is hard-coded here but I don't see any reason that you couldn't grab it from the element instead.
Improvements/suggestions welcomed.
Here's the original plunkr with the change.

This is awful, and for Angular 2+ but just for the record here's one idea.
I have two button elements, one for when the user has items in their shopping cart, and one for when they don't.
The easiest way by far is to put position: relative on the parent DIV and position: absolute on both the buttons. The main disadvantage is the parent DIV has to be sized manually, and things like centering becoming trickier.
If the intent is to delay adding to the DOM based on an Observable value, then I thought 'Why not just delay the observable value?' which will have the same end effect. This needs to be done only when the transition is from false > true though because you only want to hide it when it is coming into view. So I used a pipe to handle this.
<!-- This button for when item is IN the cart -->
<button [#cartIconAnimation] *ngIf="showCartIcon | delayTrue | async">View Cart</button>
<!-- This button for when item is NOT IN the cart -->
<button [#cartIconAnimation] *ngIf="showCartIcon | complement | delayTrue | async">Add to Cart</button>
This assumes showCartIcon is Observable<boolean>.
Then the pipes are as follows, and no delay is required on your animation criteria.
#Pipe({
name: 'delayTrue'
})
export class DelayTruePipe implements PipeTransform {
constructor() {}
transform(value: Observable<any> | any, delay: number): Observable<any> {
if (isObservable(value)) {
return value.pipe(distinctUntilChanged(), debounce((show) => show ? timer(delay || 500) : empty()));
} else {
throw Error('Needs to be an observable');
}
}
}
#Pipe({
name: 'complement'
})
export class ComplementPipe implements PipeTransform {
constructor() {}
transform(value: Observable<boolean> | any): Observable<any> {
if (isObservable(value)) {
return value.pipe(map(i => !i));
} else {
throw Error('Needs to be an observable');
}
}
}
Note: The delay used by the pipe must be greater than the time it takes for the previous item to disappear, or you'll have the same problem.
The complement pipe just inverts the boolean value.
This solution works, but it's hacky and the timing might be harder to get wrong and there may be race conditions as two different browser timers fire off at the same time. I'd only do something like this if you really can't use position: absolute.

Related

React dropzone - dragLeave event fired when dragging file over dropzone

I am using React dropzone for file upload
<DropZone
accept='.pdf,.pptx,.ppt,.docx,.doc,.xls,.xlsx,.xslx,.png,.xsl,.jpg,.jpeg,.gif,.zip'
onDrop={ files => {
this.handleFileDrop(files);
this.dragLeaveHandler();
} }
onDragEnter={ this.dragOverHandler }
onDragLeave={ this.dragLeaveHandler }
multiple={ false }
style={ { height: '100%' } }
>
dragOverHandler = () => {
console.log('enter');
this.setState({
isDragOver: true,
});
};
dragLeaveHandler = () => {
console.log('exit');
this.setState({
isDragOver: false,
});
};
When a file is moving above the drop zone onDragLeave event fires simultaneously.
Should I use some other events?
How can I fix this issue?
You could use pointer-events: none; on the element(s) that are firing the drag leave. That should still allow the dropped event and getting the accepted file though would stop overriding the dropzone events.
The problem you're facing is most likely caused by the DOM events dragEnter and dragLeave getting messed up instead of any flaw in the react-dropzone package. Some elements may cause hovering over them in certain positions not to register as hovering over their parent element. For example, there is a thin sliver at the top edge of any plain string rendered inside a block displayed element. Most commonly this happens inside a <p> tag.
Without seeing the children rendered inside your dropzone, it is impossible to give a specific fix. Generally, you will have to mess with the styling of the children, though. <p> tags for example will not be a problem if their size is set to 0 pixels or if they're replaced with <span> tags. Both options will disrupt the displaying of the children, which is unfortunatley unavoidable.
As for using other events, you're out of luck. The DropZone component relies on the onDragEnter and onDragLeave HTML DOM events. Therefore any fix you might come up with won't fix the component itself.
All in all, it's an unfortunate issue that just has to be dealt with. The simplest way to deal with it is to just have at most one piece of text inside the dropzone and to set its size to 0 pixels with css: height: 0px;. Regular <div> elements won't cause issues, so you can craft an intricate dropzone using them.

React - How to notify popper to reposition my popover whenever react updates any DOM element

Relevant versions: React 16.4.2, Bootstrap 4.1.3, popper.js 1.14.4, Typescript 3.0.3
I use the Bootstrap Popover functionality in my react app.
The Popover works well if the rest of the page is static. When the page is changed (at the browser level), the Popover gets repositioned very quickly and smoothly so it stays visible while the content it's anchored to is visible:
when scrolling if it bumps up against the windows edges
if the screen is rotated on a phone
if the window is resized
This all works well because popper.js is apparently watching the window.scroll and window.resize events, as per this answer: Bootstrap 4 - how does automatic Popover re-positioning work?
The problem comes when my react application starts showing/hiding DOM elements. Because popper.js doesn't know about react, it doesn't know the DOM changed, so it doesn't know that the Popovers might need to be repositioned.
I know calling popover("update") on each Popover anchor works, because I've added code like this to do it intermittently:
window.setInterval(()=> $(this.selfRef).popover("update"), 100);
But that's yucky and wasteful, and a little janky.
Is there a way to have react tell me when it updates any node in the DOM, so I can then tell popper.js to update the position of the popovers?
Note that the react component that causes the DOM change isn't necessarily located near the component that uses the Popover. It could be something in a completely separate part of the hierarchy that happens to be displayed before the component with the popover - so the I don't think the solution is componentWillReceiveProps() or methods like that on the Popover component, because it's probably not the component that's causing the movement.
Note that I'm aware of projects like react-bootstrap, reactstrap or react-popper - but I don't want to use them.
EDIT: it seems like MutationObserver might be a non-react way to do this. I just figured since React is already doing all that reconciliation work, maybe there's a way to get it to notify me when it actually does edit the DOM.
"The react Component that causes the DOM change isn't necessarily
located near the Component that uses the Popover. It could be
something in a completely separate part of the hierarchy"
If both the Component that changes the DOM, and the Component that creates the Popover are in the same parent, you could share a method in the parent that does the .popover('update'). The Component that changes the DOM would need to trigger this event, but it doesn't need to be specifically "aware" of the Popover Component. The Popover Component doesn't need to be aware of the DOM changing Component.
class ChangeDom extends React.Component {
constructor(props) {
super(props);
this.changeDom = this.changeDom.bind(this);
}
changeDom () {
this.props.domChanged();
}
render() {
return (
<div>
<button className="ml-2 btn btn-primary" onClick={this.changeDom}>Change Dom
</button>
</div>)
}
}
class Pop extends React.Component {
constructor(props) {
super(props);
this.togglePopover = this.togglePopover.bind(this);
}
togglePopover() {
$('[data-toggle="popover"]').popover('toggle');
}
render() {
return (
<div class="position-relative">
<button className="mt-4 btn btn-primary" onClick={this.togglePopover} data-toggle="popover"
</button>
</div>)
}
}
class Parent extends React.Component {
domChanged(){
$('[data-toggle="popover"]').popover("update");
}
render() {
return (
<div>
<ChangeDom domChanged={this.domChanged} />
<Pop />
</div>)
}
}
Demo: https://www.codeply.com/go/NhcfE8eAEY
This is my current attempt at a MutationObserver based solution.
UserApp is a component placed toward the top of the application hierarchy.
The Popover class is (over) used in various places in my application for a bunch of stuff.
The possibility of infinite recursion caused by firing popover("update") from a MutationObserver event makes me wary of using this solution long term.
It seems to do the job for now, but this is one of the things uni-directional binding is meant to avoid.
On the plus side, this works even when you have non-react components in your application (like for example, the Bootstrap navbar).
export class UserApp extends React.Component<any, AppState> {
public domChangeObservers = $.Callbacks();
public mutationObserver = new MutationObserver(
(mutations: MutationRecord[])=>{
// premature optimisation?
// I figure I don't care about each individual change, if the browser
// batched em up, just fire on the last one.
// But is this a good idea given we have to inspect the mutation in order
// to avoid recursive loops?
this.domChangeObservers.fire(mutations[mutations.length-1]);
}
);
constructor(props: any) {
super(props);
this.mutationObserver.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
}
componentWillUnmount(){
this.mutationObserver.disconnect();
}
...
}
const DefaultTrigger = "click";
export interface PopoverProps{
popoverTitle: string | Element | Function;
popoverContent: string | Element | Function;
/** Set to "focus" to get "dismiss on next click anywhere" behaviour */
popoverTrigger?: string;
/** Leaving it empty means that the popover gets created
* as a child of the anchor (whatever you use as the child of the popover).
* Setting this to "body" means the popover gets created out on the body
* of the document.
* "body" can help with stuff like when the popover ends up
* being clipped or "under" other components (because of stuff like
* `overflow:hidden`).
*/
container?: string;
allowDefaultClickHandling?: boolean;
ignoreDomChanges?: boolean;
id?: string;
}
export class Popover
extends PureComponent<PopoverProps, object> {
// ! to hack around TS 2.6 "strictPropertyInitialization"
// figure out the right way... one day
selfRef!: HTMLSpanElement;
onDomChange = (mutation:MutationRecord)=>{
/*
- popover("update") causes DOM changes which fire this handler again,
so we need to guard against infinite recursion of DOM change events.
- popover("update") is async, so we can't just use an "if not currently
handling a mutation" flag, because the order of events ends up being:
onDomChange() -> flag=true -> popover("update") -> flag=false ->
popper.js changes DOM -> onDomChange() called again -> repeat forever
- Can't just detect *this* popover. If DOM event occurs because popovers
overlay each other they will recurse alternately - i.e. pop1 update
call makes DOM changes for pop2, pop2 update makes changes for pop1,
repeat forever.
*/
if( Popover.isPopoverNode(mutation) ){
return;
}
/*
- tell popper.js to reposition the popover
- probably not necessary if popover is not showing, but I duuno how to tell
*/
$(this.selfRef).popover("update");
};
private static isPopoverNode(mutation: MutationRecord){
/*
Had a good attempt that used the structure of the mutation target to
see if it's parent element was defined as `data-toggle="popover"`; but
that fails when you set the `container` prop to some other element -
especially, "body", see the comments on the Props .
*/
if( mutation.target.nodeType != 1 ){
return false;
}
// Is Element
let element = mutation.target as Element;
/*
Is the mutation target a popover element?
As defined by its use of the Bootstrap "popover" class.
This is dodgy, it relies on Bootstrap always creating a container
element that has the "popover" class assigned.
BS could change their classname, or they could
change how they structure their popover, or some other
random widget could use the name.
Actually, this can be controlled by overriding the popover template,
which I will do... later.
*/
let isPopoverNode = element.classList.contains("popover");
// very helpful when debugging - easy to tell if recursion is happening
// by looking at the log
// console.log("target", isPopoverNode, mutation, mutation.target );
return isPopoverNode;
}
componentDidMount(): void{
// the popover() method is a "JQuery plugin" thing,
// that's how Bootstrap does its stuff
$(this.selfRef).popover({
container: this.props.container || this.selfRef,
placement: "auto",
title: this.props.popoverTitle,
content: this.props.popoverContent,
trigger: this.props.popoverTrigger || DefaultTrigger,
});
if( !this.props.ignoreDomChanges ){
UserApp.instance.domChangeObservers.add(this.onDomChange);
}
}
componentWillUnmount(): void {
if( !this.props.ignoreDomChanges ){
UserApp.instance.domChangeObservers.remove(this.onDomChange);
}
// - without this, if this component or any parent is unmounted,
// popper.js doesn't know that and the popover content just becomes
// orphaned
$(this.selfRef).popover("dispose");
}
stopClick = (e: SyntheticEvent<any>) =>{
if( !this.props.allowDefaultClickHandling ){
// without this, if the child element is an <a> or similar, clicking it
// to show/dismiss the popup will scroll the content
e.preventDefault();
e.stopPropagation();
}
};
render(){
let popoverTrigger = this.props.popoverTrigger || DefaultTrigger;
// tabIndex is necessary when using "trigger=focus" to get
// "dismiss on next click" behaviour.
let tabIndex = popoverTrigger.indexOf("focus")>=0?0:undefined;
return <span id={this.props.id}
tabIndex={tabIndex}
ref={(ref)=>{if(ref) this.selfRef = ref}}
data-toggle="popover"
onClick={this.stopClick}
>{this.props.children}</span>;
}
}

How to wait and fade an element out?

I have an alert box to confirm that the user has successfully subscribed:
<div className="alert alert-success">
<strong>Success!</strong> Thank you for subscribing!
</div>
When a user sends an email, I'm changing the "subscribed" state to true.
What I want is to:
Show the alert box when the subscribed state is true
Wait for 2 seconds
Make it fade out
How can I do this?
May 2021 update: as tolga and Alexey Nikonov correctly noted in their answers, it’s possible to give away control over how long the alert is being shown (in the original question, 2 seconds) to the transition-delay property and a smart component state management based on the transitionend DOM event. Also, hooks are these days recommended to handle component’s internal state, not setState. So I updated my answer a bit:
function App(props) {
const [isShowingAlert, setShowingAlert] = React.useState(false);
return (
<div>
<div
className={`alert alert-success ${isShowingAlert ? 'alert-shown' : 'alert-hidden'}`}
onTransitionEnd={() => setShowingAlert(false)}
>
<strong>Success!</strong> Thank you for subscribing!
</div>
<button onClick={() => setShowingAlert(true)}>
Show alert
</button>
(and other children)
</div>
);
}
The delay is then specified in the alert-hidden class in CSS:
.alert-hidden {
opacity: 0;
transition: all 250ms linear 2s; // <- the last value defines transition-delay
}
The actual change of isShowingAlert is, in fact, near-instant: from false to true, then immediately from true to false. But because the transition to opacity: 0 is delayed by 2 seconds, the user sees the message for this duration.
Feel free to play around with Codepen with this example.
Since React renders data into DOM, you need to keep a variable that first has one value, and then another, so that the message is first shown and then hidden. You could remove the DOM element directly with jQuery's fadeOut, but manipulating DOM can cause problems.
So, the idea is, you have a certain property that can have one of two values. The closest implementation is a boolean. Since a message box is always in DOM, it's a child of some element. In React, an element is result of rendering a component, and so when you render a component, it can have as many children as you want. So you could add a message box to it.
Next, this component has to have a certain property that you can easily change and be completely sure that, as soon as you change it, the component gets re-rendered with new data. It's component state!
class App extends React.Component {
constructor() {
super();
this.state = {
showingAlert: false
};
}
handleClickShowAlert() {
this.setState({
showingAlert: true
});
setTimeout(() => {
this.setState({
showingAlert: false
});
}, 2000);
}
render() {
return (
<div>
<div className={`alert alert-success ${this.state.showingAlert ? 'alert-shown' : 'alert-hidden'}`}>
<strong>Success!</strong> Thank you for subscribing!
</div>
<button onClick={this.handleClickShowAlert.bind(this)}>
Show alert
</button>
(and other children)
</div>
);
}
}
Here, you can see that, for message box, either alert-shown or alert-hidden classname is set, depending on the value (truthiness) of showingAlert property of component state. You can then use transition CSS property to make hiding/showing appearance smooth.
So, instead of waiting for the user to click button to show the message box, you need to update component state on a certain event, obviously.
That should be good to start with. Next, try to play around with CSS transitions, display and height CSS properties of the message box, to see how it behaves and if the smooth transition happening in these cases.
Good luck!
PS. See a Codepen for that.
The correct way is to use Transition handler for Fade-in/out
In ReactJS there is synthetic event to wait till fade-out is finished: onTransitionEnd.
NOTE there are different css effects associated with different handlers. Fade is a Transition not an Animation effect.
Here is my example:
const Backdrop = () => {
const {isDropped, hideIt} = useContext(BackdropContext);
const [isShown, setState] = useState(true);
const removeItFromDOM = () => {
debugger
setState(false)
};
return isShown
? <div className={`modal-backdrop ${isDropped ? 'show' : ''} fade` } onClick={hideIt} onTransitionEnd={removeItFromDOM}/>
: null
}
An other way is to solve this with a CSS3 transition.
https://www.tutorialspoint.com/css/css_animation_fade_out.htm
You can add a new class to the alert (like .hidden) and then you can relate .hidden with the class you defined for the alert.
alert.hidden{
// Here you can define a css transition
}
In this solution you don't have to add a setInterval or anything, since css3 transitions already process it on browser render.

Angular performance when drag drop : various targets and highlighting

I have, listing of many separate items (or tiles) in a list on my page. If the user drags a file over the an item in the list, I conditionally add the highlight class in the onDragEnter and remove the class in the onDragLeave. The highlight class essentially changes the background-color of the item.
Without using a $scope.$apply, the highlight takes a second or so before being reflected in the UI, so I am wrapping the highlight within the $scope.$apply.
Now when I move the file quickly between items, the performance becomes really slow (the highlighting and un-highlighting becomes very slow) due to the large number of digest cycles. Any ideas on how I can improve the performance but at the same time have the changes reflected in the UI pretty quickly.
I tried using $scope.$digest() instead of $scope.$apply but did not notice any improvement in the performance.
My code is:
function onDragEnter() {
$scope.$apply(function() {
$scope.highlight = true; // this controls if the highlight class is added to the item using ng-class
});
}
function onDragLeave() {
$scope.$apply(function() {
$scope.highlight = false;
});
}
In case this helps someone, using $scope.$evalAsync helped improve the performance significantly.
function onDragEnter() {
$scope.$evalAsync(function($scope) {
$scope.highlight = true; // this controls if the highlight class is added to the item using ng-class
});
}
function onDragLeave() {
$scope.$evalAsync(function($scope) {
$scope.highlight = false;
});
}

Stop event firing on child elements using EXT's mouseenter

The Problem:
I have an anchor tag with a class name 'hasChildren' which in turn has a span element containing the text. When using EXT's .on('mouseenter',function()) on the anchor tag, it fires the event on both the span and/or the anchor tag.
expected result:
hovering over either the span or the anchor tag, the class should be added to the anchor tag alone
current result:
hovering over either the span or the anchor tag, the class is added to the element which receives focus first.
As in the JS you'll see I tried the hover function but gives the same results.
The HTML:
<a class="hasChildren" href="#"><span>web 2.0</span></a>
The CSS:
.hasChildren {
display:block;
width:100px;
background-color:#333;
}
.hasChildren span {
background-color:#EEE;
display:block;
line-height:40px;
margin-left:10px;
padding:0 20px 10px 10px;
}
The JavaScript:
function over(e,t){
Ext.get(t).addClass('red');
}
function out(e,t){
Ext.get(t).removeClass('red');
}
Ext.onReady(function() {
//Ext.select('.hasChildren').hover(over,out,this);
Ext.select('.hasChildren').on('mouseenter',over);
Ext.select('.hasChildren').on('mouseleave',out);
});
FYI: I'm using ext-core-3.1.0 and I can get this working by using jQuery but as the lead developer requested that I only use extJS, I'd like to get this working without adding another javascript library.
Use this function of Ext.Element : addClassOnOver(). So, for your case, it will be:
Ext.onReady(function() {
Ext.select('.hasChildren').addClassOnOver('red');
});
It will automatically toggle the CSS class.
Try and surround your anchor tag in a div and then attach the listeners on the div. I think you want to try and attach the events to the outermost container in this case.
I eventually found what I was looking for. The javascript functions should change to look like this:
function over(e,t){
Ext.get(e.getTarget('a')).addClass('red');
}
function out(e,t){
Ext.get(e.getTarget('a')).removeClass('red');
}
The explanation: Previously I tried to add the class to 't' as in get(t), which rightfully could be either the parent or child element. But by using (e.getTarget('a')) I tell it to select the anchor tag and apply the class to that element alone.
This method gives one control over propagation, funny thing is the following could also work for the 'out' function and it would do exactly the same (in theory):
function out(e,t){
Ext.get(e.getTarget('span')).parent().removeClass('red');
}
Another thing I discovered: The Ext.onReady functions can also be written as:
Ext.select('.hasChildren').on( { mouseenter: { fn: over } } );
This way makes it easier to add more events to the target element(s)
Ext.select('.hasChildren').on( { mouseenter: { fn: over } }, { mouseleave: { fn: out } });

Resources