Angular custom complex router transitions - html

Currently I'm trying to implement some route transition. To do so, I'm using a component that is being displayed on click calling its respective service's function:
routeTransition(destination) {
if (this.router.url !== destination) {
this.ls.startLoading(destination)
}
}
startLoading(destination) {
if (this.loading.getValue() === 0) {
this.loading.next(1);
setTimeout(() => {
this.router.navigate([destination]);
}, 750)
}
}
As you can see I kind of delay the navigation - I do so, since my route transition is a black div easing in from the bottom - I timed the navigation to change the route just when the screen is covered. Afterwards, inside of the new component, I call the service's stopLoading function, which hides the transitioning div again by easing out.
That's the transition I'm talking about:
It works, but I reckon that's not the prefect way, since it won't work when the user's navigating back. What's the correct approach to implement such a transition? Is this possible using Angular's browser animations?

Using a setTimeout is indeed a bad thing because this is approximate.
Angular can provide animation especially for the router transitions.
You can add a directive to your router outlet to trigger an Angular animation on page change.
For example on your app.component.html:
<div [#routeAnimations]="prepareRoute(outlet)">
<router-outlet #outlet="outlet"></router-outlet>
</div>
app.component.ts
public prepareRoute(routerOutlet: RouterOutlet): string {
return routerOutlet && routerOutlet.activatedRouteData && routerOutlet.activatedRouteData[ 'animation' ];
}
In this example, the prepareRoute will use the animation name directly from your route custom data by using the property animation.
This way, it allows to have animation only on specific pages.
You also have to register inside the component the animations.
#Component({
animations: [
PAGES_ANIMATION
],
Then define the type of animation between your routes.
export const PAGES_ANIMATION = trigger('routeAnimations', [
transition(`home => register`, SLIDE_RIGHT_ANIMATION),
transition(`register => home`, SLIDE_LEFT_ANIMATION),
And finally, create an Angular animation.
export const SLIDE_RIGHT_ANIMATION = [
style({
position: 'relative'
}),
query(':enter, :leave', [
style({
height: '100vh',
left: 0,
overflowY: 'hidden',
position: 'absolute',
top: 0,
width: '100%'
})
]),
query(':enter', [
style({
transform: 'translateX(100%)'
})
]),
query(':leave', animateChild()),
group([
query(':leave', [
animate('400ms ease-out', style({
transform: 'translateX(-100%)'
}))
]),
query(':enter', [
animate('400ms ease-out', style({
transform: 'translateX(0%)'
}))
])
]),
query(':enter', animateChild())
];
If this is a correct answer for you but you can not make it, ask me for help.
You can also read the documentation to understand the concepts and see a real world example.

It turns out I was attempting to create an animation exactly like that in my Angular App. My solution is as follows :
in app.component.html, wrap your router outlet within a div and add the elements you want to animate above the router outlet but within the same parent.
<div class="position-relative" [#parentRoute]="getRouteAnimationData()" >
<div class="position-fixed Curtain" style="height:0%" ></div>
<router-outlet></router-outlet>
</div>
in app.component.ts , inject ChildrenOutletContexts to get your data from the route snapshot to compare previous and next routes and return that data as input for your animation
constructor(private contexts:ChildrenOutletContexts){}
getRouteAnimationData(){
return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation']
}
There are many ways to go about creating animation similar to the one you are trying to achieve. I found that the easiest way was to separate the animation in 2 steps : The entrance animation (This is where the curtain comes down, and shows your logo ), and the exit animation (This is where the logo fades out, then the curtain disappears, and the user finds himself on the new route).
This can be easily achieved by using a combination of delays and animation sequences provided by the angular animation engine. It's cleaner to write your animations in a separate file instead of in the template to improve readability and allow reusability throughout the app.
create a file called app.animation.ts, and declare a function that will be used for your route transition (in this case, closeOpenCurtain()), and export the trigger as a const that will be imported into the components which will use the animation (In this case, parentRoute)
export const parentRoute =
trigger('parentRoute', [
transition('home => *', closeOpenCurtain()),
transition('contact => *', closeOpenCurtain()),
])
function closeOpenCurtain() {
return [
// Set basic styles so the DOM cleanly removes/inserts the old and new route
style({position:'relative', overflow:'hidden'}),
query(':leave',[
style({opacity:1})
],{optional:true}),
query(':enter',[
style({position:'absolute',top:0,left:0,width:'100%'})
], {optional:true}),
// First sequence
// Descend the curtain upon the viewport, and delay the opacity of the new
// route until the curtain has fully descended
group([
query('.Curtain',[
animate('450ms cubic-bezier(0.87, 0, 0.13, 1)', style({ height: '100%' })),
], {optional:true}),
query(':enter', [
style({ opacity: 0 }),
animate('1ms 450ms', style({ opacity: 1 }))
], { optional: true }),
]),
// Second sequence
// Make old route disappear immediately, then
// slide the curtain out of the viewport
group([
query(':leave', [
style({opacity:0})
//animate('1ms', style({ opacity: 0 }))
], { optional: true }),
query('.Curtain',[
animate('450ms cubic-bezier(0.87, 0, 0.13, 1)', style({ height: '0%' })),
], {optional:true}),
])
]
}
I will not go too much into detail about how triggers and complexe animation sequences work in angular, as you can find all the documentation for it on the website : https://angular.io/guide/complex-animation-sequences
However, I will briefly describe what is happening in the above snippet.
Basic styles :
First, you set basic styles on the parent template which contains the router outlet. Angular always inserts the entering component directly beneath the existing component during on route change. by setting the entering route as absolute and invisible allows the new route to enter the DOM cleanly without affecting the layout during the transition. This will be more or less similar in other variations of a route transition.
First sequence :
By creating the first sequence as a group, you allow any animation declared within the group to run synchronously. First, we query the element we wish to animate, in this case, we reference the .Curtain. The curtain will descend upon the screen for 450ms as it's height animate from 0% to 100%. To prevent the new route from appearing before the curtain has descended, we animate the new route with a delay of 450ms (to give enough time for the curtain to have descended before showing the new route).
Second sequence (begins after the first sequence has completed) :
The second sequence is similar to the first one, only that it sets the curtain's height back to 0%, and makes the previous route disappear by setting it's opacity to 0.
the trigger :
The animation directive will be triggered whenever the route changes, in my case, I did not want the animation to trigger when the page reloads. In order to prevent this, I allow the transition to detect when the router-outlet is navigating "away" from an existing route, regardless of the route it navigates towards.
Finally, make sure you declare your animation data within your app-routing.module.ts to allow your transitions to work :
{
path: 'home', data:{animation: 'home'},
loadChildren: () => import('./home/home.module').then(m => m.HomeModule)
},
{
path: 'contact', data:{animation: 'contact'},
loadChildren: () => import('./contact/contact.module').then(m => m.ContactModule)
},
The above animation will work when you use your browser's navigation. If you want to add the extra bit where your logo fades in, add an extra step in the first and/or second sequence by querying that logo's class name. There are many ways to make this animation work, but I sincerely believe this is the easiest way to get it done.
I hope this helps you make your angular apps look even more awesome !

Related

Make absolutely positioned element [ rendered using React ] snap to edge of parent div on page load?

I am making a two-thumb slider bar component in React, where each thumb snaps to the closest of discrete ticks so a user can visually pick from values along a number line. The problem is, as it stands the thumbs need to have position: absolute, but their parent div slots into a regular responsive layout, so the thumbs don't know ahead of time where they're going to need to be on the page - they need to get that information somehow from the parent div itself, on page load. I tried getting my useRef() to the parent div and setting the thumbs' positions within a useEffect(() => {},[]), but apparently that's not possible. How can I tell a child, the position of its parent, immediately on page load in React?
I came up with a hacky solution, that works: I pass the child component a the ref to the parent, and have it set its own position state repeatedly every 10ms [ so from here I have the choice I guess between toning it down to something less embarrassingly overblown but jankier-looking or finding an entirely different solution! ] from within this useInterval hook, so that when the ref to the parent becomes defined, the child snaps there immediately. There are still other bugs, but here is the working Thumb component:
import React, { useEffect, useState, useRef } from 'react';
import { useInterval } from '../utils';
function Thumb(props) {
const { thumb_key,
snap_tick,
bar_ref,
thumb_ref,
color,
n_ticks,
thumb_on_mouse_down } = props;
const [pos,set_pos] = useState(0);
let my_width;
let bar_start;
let bar_width;
useInterval(() => {
my_width = thumb_ref.current.getBoundingClientRect().width;
bar_start = bar_ref.current.getBoundingClientRect().left;
bar_width = bar_ref.current.getBoundingClientRect().width;
set_pos( ( bar_start + ( snap_tick * bar_width ) / (n_ticks - 1) - Math.floor(my_width / 2) ) );
},10);
return (
<div className="thumb-outer"
ref={thumb_ref}
style={{
height: '20px',
width: '20px',
borderRadius: '50%',
backgroundColor: color,
position: 'absolute',
left: pos + 'px',
cursor: 'grab',
dataKey: thumb_key,
}}
onMouseDown={e => thumb_on_mouse_down(e, thumb_key)}
>
</div>
)
};
export default Thumb;

Multilevel mat-table does not expand to another level after first sort

We have been using multiTemplateDataRows for multilevel mat-table. Its working fine and we can expand it to multi level. Although, there seems to be one issue in specific scenario.
Scenario:
Sort level-1 once application loads data in to the table
click on any row and it will not expand
Again click on another row, it will expand. Now it will expand on every click, issue will no longer exist.
Its just not working when we expand very first time after sort.
below is the HTML for level-1:
<table mat-table #outerSort="matSort" [dataSource]="dataSource" multiTemplateDataRows class="mat-elevation-z8" matSort>
and typescript:
#ViewChild('outerSort', { static: true }) sort: MatSort;
We couldn't found any logs or error in console, so not sure what is issue.
here is stalkblitz for same: https://stackblitz.com/edit/angular-nested-mat-table-expand-issuw-when-sort
So, I found an answer.
We have been trying to find issue in data and sort behavior but issue is animation.
Angular Animations setting animation state to void when items are sorted and that what happening.
as a solution either remove animation (if you can) else apply following void in animation path.
trigger("detailExpand", [
state(
"collapsed, void",
style({
height: "0px",
visibility: "hidden"
})
),
state(
"expanded",
style({
"min-height": "48px",
height: "*",
visibility: "visible"
})
),
transition(
"expanded <=> collapsed, void <=> *",
animate("225ms cubic-bezier(0.4, 0.0, 0.2, 1)")
)
])
Stalkblitz already updated.
Ref: https://github.com/angular/components/issues/13835

Best way to dim/disable a div in Material-UI?

In my app, I have divs that I want to dim and disable mouse events for, depending on component state - for example, loading. The initial method I came up with is to have a helper function that returns an inline style for dimming an element and disabling pointer events on it, given a boolean value:
const disableOnTrue = (flag) => {
return {
opacity: flag ? 0.15 : 1,
pointerEvents: flag ? "none" : "initial"
}
}
and using it on elements as such:
{loading && {/** render a loading circle */}}
<div style={disableOnTrue(this.state.loading)}>{/** stuff to be dimmed & disabled while loading */}</div>
In the disabled div, there are Material-UI Buttons. However, it turns out that they don't care if pointerEvents are disabled on their parent div, and remain clickable, which is a big problem. So, on the Buttons I had to set disabled={loading}. Then, this dims the Buttons themselves, which unnecessarily compounds with the lowered opacity of disableOnTrue, meaning I would need to add some custom styling to ameliorate that; I want the entire div to be disabled, not for the Button to look especially disabled.
I've also tried using the Backdrop component from Material, but couldn't get it to dim anything but the entire viewport.
Before I implement any sort of hacky solution throughout my entire app, I figured I should ask here to see if there is a clean way to achieve this that I'm missing. I've looked for quite a while, but haven't found anything.
I split the concept of "disabling" into two functions:
const dimOnTrue = (flag) => {
return {
opacity: flag ? 0.15 : 1,
}
}
const disableOnTrue = (flag) => {
return {
pointerEvents: flag ? 'none' : 'initial'
}
}
to be used on divs that should be dimmed and inputs that should be disabled, respectively.

Show a specific frame on initial load in lottie animation

I have a lottie animation that I have integrated it in my code. I want to show a specific frame on initial load and when the user hover it the animation should start from the beginning and completes it.
This is what I want to show in first load -
And this is the complete animation -
This is my code
let iconMenu = document.querySelector('.bodymovinanim1');
let animationMenu = bodymovin.loadAnimation({
container: iconMenu,
renderer: 'svg',
loop: false,
autoplay: false,
path: "https://assets2.lottiefiles.com/packages/lf20_txJcSM.json",
});
animationMenu.addEventListener('DOMReady',function(){
animationMenu.playSegments([1,200],true);
});
iconMenu.addEventListener('mouseover', (e) => {
animationMenu.play();
});
This is my fiddle
Can anyone please help me?
It looks like you're using an outdated version of Lottie in this fiddle. Once that's updated you probably want to pass through the initialSgments option instead of using playSegments. In this case you can remove your DOMReady code block entirely and it works as expected. The one issue with your animation is the circle by itself doesn't actually exist. There's a tiny dot of green on the only half frame where that circle is completed before the rest starts animating. 51.5 is the closest from you can get, here's a fiddle showing it
let iconMenu = document.querySelector('.bodymovinanim1');
let animationMenu = bodymovin.loadAnimation({
container: iconMenu,
renderer: 'svg',
loop: false,
autoplay: false,
path: "https://assets2.lottiefiles.com/packages/lf20_txJcSM.json",
initialSegment: [51.5, 200],
});
iconMenu.addEventListener('mouseover', (e) => {
animationMenu.play();
});

Fx.Reveal event when done (complete)

hi sorry completely new to mootools used to jquery, have a container (saved to variable itemContent) which reveals,
after this a function galleryScroll is call which scrolls the element to the container saved to var itemScroll,
want to make sure itemContent is revealed before scroll function is called whats the best way to do this?
thanks
itemContent.reveal({
'height' : '100%',
duration: 1600,
}).addClass('open-content');
// should this fire this in a callback function so it fires once the container is revealed
galleryScroll.toElement(itemScroll);
Fx.Reveal extends Fx and as such, inherits all of it's events.
try via the element setter:
itemCount.set('reveal', {
onComplete: function(){
galleryScroll.toElement(this.element);
}
}.reveal({... });
you can also get the reveal instance:
var fxReveal = itemCount.get('reveal');
this will return the instance and you can set whatever you like to it like usual.
You can enable chaining with the link option.
itemContent.reveal({
'height': '100%',
duration: 1600,
link: 'chain'
}).addClass('open-content');
This example will hide, reveal and then alert. Please note that I need to get the reveal instance as the standard Element in mootools does not implement the Chain class.
document.id('first').set('reveal', {
duration: 'long',
transition: 'bounce:out',
link: 'chain'
}).dissolve().reveal().get('reveal').chain(function(){alert('message');});
To see it in action: http://jsfiddle.net/LKSN8/1/