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

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;

Related

How can I get the height of an element after it has rendered?

I want to get the height after an element is rendered. The element is inside another component than which I want to detect it in.
I know I can do it like this:
const heightOfStepper = document.getElementsByClassName(
"step-indicator"
)?.[0]?.offsetHeight;
const minHeight = useMemo(
() => `calc(${heightOfStepper}px + var(--small-spacing))`,
[heightOfStepper]
);
But when I console.log out the heightOfStepper, the value is 0 the first render, and if a add some code to my IntelliJ and save, then I get the real height of the stepper, since then the stepper is already mounted.
How can I get the height of an element after it has rendered, and the component is inside another component? (So I can not just use useEffect(() => ..., []))
In react you should avoid regular DOM queries (like getElementsByClassName) - use ref instead. What do you mean you cannot use useEffect? You can use it even in deeply nested component. If you want to get height of parent element you will want to use forward ref, e.g. like this:
// parent component
const App = () => {
const ref = useRef(null);
return (
<div ref={ref}>
MyDiv
<Child ref={ref} />
</div>
);
};
// child component
const Child = React.forwardRef((props, ref) => {
useEffect(() => {
console.log(ref.current.offsetHeight);
}, []);
return <p>I'm a child component</p>;
});
If your find yourself in need of passing ref deep through components tree or e.g. between siblings - pass ref through context - that was answered here

How to reserve space for responsive img (stop blocks jumping down)

How to solve a problem of jumping down blocks after image load if image should be responsive?
Codesandbox.
Info:
Image should shrink on window resize
Image max-width is it's own width
We don't know image size
JS can be used
Idea:
If it's not possible without any image size data, then can it be done only with image aspect ratio?
If you know image aspect ratio, you can calculate block height and put responsive image inside with some unused space (at least unused space will be not big on small window size).
Image loading:
Image loaded:
Html:
import React from "react";
import "./styles.css";
const img =
"https://s3.amazonaws.com/images.seroundtable.com/google-css-images-1515761601.jpg";
export default function App() {
return (
<div className="app">
<img alt="" src={img + `?${Date.now()}`} className="img" />
<h1>I always jump down :(</h1>
</div>
);
}
CSS:
.app {
box-sizing: border-box;
}
.img {
max-width: 100%;
}
h1 {
background-color: red;
}
AS #DBS sad, it seems that only solution is to wrap image in div block with calculated height based on window.width and aspect ratio (in my case i know it). If there is any better solutions, don't hesitate to answer.
Sandbox solution example.
import React from "react";
import "./styles.css";
import styled from "styled-components";
const aspectRatio = [1, 5];
const img =
"https://s3.amazonaws.com/images.seroundtable.com/google-css-images-1515761601.jpg";
export default function App() {
return (
<div className="app">
<StyledDiv>
<img alt="" src={img + `?${Date.now()}`} className="img" />
</StyledDiv>
<h1>I always jump down :(</h1>
</div>
);
}
const StyledDiv = styled.div`
height: calc(100vw / ${aspectRatio[1]});
overflow-y: hidden;
`;
As you've already determined, even when knowing the aspect ratio, we still have the problem of not knowing the width of the image, making the aspect ratio useless if we are to conform to your requirement Image max-width is it's own width.
What would work is a build time solution (if you know your image urls at build time). There are plenty of server side rendering options for react apps and I will not elaborate on them.
Instead there's a super rudimentary DIY approach.
Create a Node script to run on your server. It will build JSON file with image dimensions. Let's call it "buildImageDimensionData.js". It will output a JSON file imageDimensions.json.
var probe = require("probe-image-size"); // For getting image dimensions without downloading full image
const fs = require("fs"); // For writing our image dimensions to file
// Supply the image urls by hand, or build a webpack script to extract them etc...
const imageUrls = [
`http://localhost:5000/image1.jpg`,
`http://localhost:5000/image2.jpg`,
];
async function getImageDimensionsByUrl(urlList) {
const probePromises = urlList.map(async (imageUrl) => await probe(imageUrl));
let probedImagesData = [];
try {
probedImagesData = await Promise.all(probePromises);
} catch (error) {
// We are programming gods and there will be no error.
}
return probedImagesData.reduce((accumulator, imageData) => {
accumulator[imageData.url] = [imageData.width, imageData.height];
return accumulator;
}, {});
}
getImageDimensionsByUrl(imageUrls).then((dimensions) =>
fs.writeFileSync("imageDimensions.json", JSON.stringify(dimensions))
);
Run the script before build. For example, if you're using Create React App, in package.json:
...
"start": "buildImageDimensionData && react-scripts start",
"build": "buildImageDimensionData && react-scripts build",
...
In your react image component, import imageDimensions.json and use the data to set dimensions for image wrapper based on url of the image.
Note this is more for demonstraing how it could work and there will definitely be packages for doing this better.
Another, albeit poor option is of course just waiting for the images to load before rendering your other content so no jumps occur.
To ignore the jump down, you have to set margin of the next sibling to 0,
.app {
box-sizing: border-box;
}
.img {
max-width: 100%;
}
.app :nth-child(2) {
margin:0px
}

is the renderProps pattern possible with polymer's lit-element?

There is a very useful pattern in react called the renderProps pattern (https://reactjs.org/docs/render-props.html) but I'm not sure if this is possible with lit-elements, due to the way the shadow dom isolates the css (meaning any css defined on the renderProp won't be carried into the shadow dom of the component with the renderProp).
Has anyone found a way around this, or a different pattern that enables the same use case as the renderProps pattern ?
Thanks !
EDIT: Here is an example that might make it clearer. Let's imagine a hover-menu component whose job is to display a menu on hover. This menu might need to know the position of the element hovered. And we obviously want to be able to render whatever we want inside it.
So we would like to be able to do something like that (renderMenuContent is a renderProp).
<hover-menu
.renderMenuContent="${(boundingClientRect) =>
html`<div>my menu content which could be positioned using ${JSON.stringify(boundingClientRect)}</div>`
}"
></hover-menu>
Turns out there is indeed no such easy solution as in React, again due to the isolation of the shadow dom.
The best solution is to create a component and use it in the renderProp (this way it can manage its own css classes).
In our example:
<hover-menu
.renderMenuContent="${(boundingClientRect) =>
html`<my-menu-content .boundingClientRect="${boundingClientRect}"></my-menu-content>`
}"
></hover-menu>
class MyMenuContent extends LitElement {
static get properties() {
return { boundingClientRect: { type: Object } };
}
static get styles() {
return css`.my-container { color: red }`;
}
render() {
return html`<div class="my-container">
can be positioned using ${JSON.stringify(this.boundingClientRect)}
</div>`;
}
}

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.

Angular custom complex router transitions

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 !