With gatsby-image, I'm swapping through some photos using setInterval() and changing the src, like so:
componentDidMount() {
this.setState({
intervalFunction: setInterval(this.imageCycle, 10000),
});
}
componentWillUnmount() {
clearInterval(this.intervalFunction);
}
imageCycle() {
let newImage = this.state.equiptmentCurrent + 1;
if (newImage >= this.state.equiptmentImages.length) {
newImage = 0;
}
this.setState(state => ({
equiptmentCurrent: newImage,
}));
}
render method:
<IMG
sizes={this.state.equiptmentImages[this.state.equiptmentCurrent]}
outerWrapperClassName="coverOuter"
position="absolute"
style={gatsbyImgStyle}
/>
is there any way to put a transition on this when the source changes?
Here's a possible approach:
Stack two tags on top of eachother via position: absolute
Style both of them with transition: opacity 1s ease-in-out;
Place a new showFront: true property on this.state.
On the componentDidMount interval hook:
Update the next images sizes (via the state obj) for the component that isn't active.
Add Opacity of 1 and 0 (respectfully) on each component depending on value of showFront. You can conditionally add a new class with something like: className={"my-image-class " + (this.state.showFront ? 'seen' : 'not-seen')} (and reversed for the bottom image). In styled-components, can do this by passing showFront as a prop.
Toggle showFront via the componentDidMount setInterval hook.
Here is my CrossFadeImage implementation. It similar to img except that it can handle the animation for you when detecting props.src changes and has extra props to customize the transition
import React from "react";
const usePrevious = <T extends any>(value: T) => {
const ref = React.useRef<T>();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const useRequestAnimationFrame = (): [(cb: () => void) => void, Function] => {
const handles = React.useRef<number[]>([]);
const _raf = (cb: () => void) => {
handles.current.push(requestAnimationFrame(cb));
};
const _resetRaf = () => {
handles.current.forEach((id) => cancelAnimationFrame(id));
handles.current = [];
};
return [_raf, _resetRaf];
};
type ImageProps = {
src: string;
alt?: string;
transitionDuration?: number;
curve?: string;
};
const CrossFadeImage = (props: ImageProps) => {
const { src, alt, transitionDuration = 0.35, curve = "ease" } = props;
const oldSrc = usePrevious(src);
const [topSrc, setTopSrc] = React.useState<string>(src);
const [bottomSrc, setBottomSrc] = React.useState<string>("");
const [bottomOpacity, setBottomOpacity] = React.useState(0);
const [display, setDisplay] = React.useState(false);
const [raf, resetRaf] = useRequestAnimationFrame();
React.useEffect(() => {
if (src !== oldSrc) {
resetRaf();
setTopSrc("");
setBottomSrc("");
raf(() => {
setTopSrc(src);
setBottomSrc(oldSrc!);
setBottomOpacity(99);
raf(() => {
setBottomOpacity(0);
});
});
}
});
return (
<div
className="imgContainer"
style={{
position: "relative",
height: "100%"
}}
>
{topSrc && (
<img
style={{
position: "absolute",
opacity: display ? "100%" : 0,
transition: `opacity ${transitionDuration}s ${curve}`
}}
onLoad={() => setDisplay(true)}
src={topSrc}
alt={alt}
/>
)}
{bottomSrc && (
<img
style={{
position: "absolute",
opacity: bottomOpacity + "%",
transition: `opacity ${transitionDuration}s ${curve}`
}}
src={bottomSrc}
alt={alt}
/>
)}
</div>
);
};
export default CrossFadeImage;
Live Demo
Related
I have the following code where changes on the parent component cause child element re - render. Basically the Menu component should be appear by right click on top of the placeholder tag but when it appears the whole parent component flickers. I used Usecallback with no luck. I tried useMemo but it doesn't accept any arguments. Since my callback functions are firing as a result of events, passing target of the event is important. Therefore I should pass the argument. I appreciate any suggestion.
const [menu, setMenu] = useState({isActive: false, position: undefined});
<div className='placeholder'
onClick={clickHandler}
onContextMenu={rightClickHandler}>
{menu.isActive && <Menu menu={menu} />}
{[props.entity].map(plc => {
let Content = place[props.entity];
if(Content) {
return <Content key={Math.random()} />
}
})}
</div>
const rightClickHandler = useCallback((e) => {
e.preventDefault();
const parentPosition = e.target.getBoundingClientRect();
const position = {
left: e.clientX - parentPosition.left,
top: e.clientY - parentPosition.top
};
setMenu(
{
isActive: (menu.isActive ? menu.isActive: !menu.isActive),
position: {
left: position.left,
top: position.top
}
}
);
}, []);
const clickHandler = useCallback((e) => {
setMenu({isActive: false, module: '', position: undefined});
}, []);
You don't need useCallback for this if you use this way. I hope this solves it.
const rightClickHandler = (e) => {
e.preventDefault()
const parentPosition = e.target.getBoundingClientRect()
const position = {
left: e.clientX - parentPosition.left,
top: e.clientY - parentPosition.top,
}
setMenu((menu) => {
return {
isActive: menu.isActive ? menu.isActive : !menu.isActive,
position,
}
})
}
Remove Math.random() It will reinforce the component to re render
const [menu, setMenu] = useState({isActive: false, position: undefined});
<div className='placeholder'
onClick={clickHandler}
onContextMenu={rightClickHandler}>
{menu.isActive && <Menu menu={menu} />}
{[props.entity].map((plc, i) => {
let Content = place[props.entity];
if(Content) {
return <Content key={'somethingElse' + i} />
}
})}
</div>
const rightClickHandler = useCallback((e) => {
e.preventDefault();
const parentPosition = e.target.getBoundingClientRect();
const position = {
left: e.clientX - parentPosition.left,
top: e.clientY - parentPosition.top
};
setMenu(
{
isActive: (menu.isActive ? menu.isActive: !menu.isActive),
position: {
left: position.left,
top: position.top
}
}
);
}, []);
const clickHandler = useCallback((e) => {
setMenu
({isActive: false, module: '', position: undefined});
}, []);
i try to make a loading screen while waiting for all images are fully loaded.
React Lifecycle is Render -> componentDidMount -> render, my images are not fully loaded, just got called but my componentDidMount always finishes and executes render even my image isn't fully loaded.
componentDidMount() {
const ie = [document.querySelectorAll('img')];
ie.map(imgElm => {
for (const img of imgElm) {
if (!img.complete) {
this.setState({ imageIsReady : true});
}
}
return this.setState({ imageIsReady : false});
})
}
on the componentDidMount for loop function try to check every img is complete or not, give me a hundred true (my image is a lot, just try to make gallery). and loading screen shows but only a few ms, then I can scroll over my image but more than half of my image is still loading.
render() {
<div>
{
this.state.imageIsReady ?
<div className='inset-0 fixed flex justify-center z-20 w-full h-full bg-black bg-opacity-25 blur'>
<img src={loading} className='w-3/12' alt="load"/>
</div> :
<div className='hidden '>
<img src={loading} alt="load"/>
</div>
}
<div>page......</div>
</div>
}
my code: https://alfianahar.github.io/MobileLegendHeroList/ in this site I use setTimeout on my componentDidMount, this does not solve my problem when using slow 3g nor fast 3g/
Maybe this example can help you. But remember that will works only for image which aren't nested inside components.
class Component extends Component {
constructor(props) {
super(props)
this.state = {
ready: false
}
}
componentDidMount() {
Promise.all(
Array.from(document.images)
.filter(img => !img.complete)
.map(img => new Promise(
resolve => { img.onload = img.onerror = resolve; }
))).then(() => {
this.setState({ ready: true })
});
}
render() {
if ( ! this.state.ready ) return <div>Loader</div>
return <div>Content</div>
}
}
<Container>
<img/> <!-- work -->
<Component>
<img/> <!-- doesn't work -->
</Component>
</Container>
React 16.8.x
import React from "react";
function App() {
const [imagesRequested, setImagesRequested] = React.useState({});
const [images, setImages] = React.useState([
{ name: "first image", src: "https://picsum.photos/200/300" },
{ name: "second image", src: "https://picsum.photos/300/300" }
]);
return (
<React.Fragment>
{images.map((currentImage) => (
<React.Fragment>
{!imagesRequested[currentImage.name] && <div>loading...</div>}
<img
style={{ opacity: imagesRequested[currentImage.name] ? 1 : 0 }}
src={currentImage.src}
onLoad={() => {
setTimeout(() => { // Fake server latency (2 seconds for per image)
setImagesRequested((previousState) => ({
...previousState,
[currentImage.name]: true
}));
}, 2000);
}}
/>
</React.Fragment>
))}
</React.Fragment>
);
}
export default App;
Your best bet is to possibly create a LoadableImage component which will then handle onLoad event for an object. This onLoad event can then call a parent callback function to set its loaded status.
LoadableImage.js
import { useState } from "react";
const LoadableImage = (props) => {
const { src, alt, width, height, onLoad, onError, id } = props;
//you can use this to render a custom broken image of some sort
const [hasError, setHasError] = useState(false);
const onLoadHandler = () => {
if (typeof onLoad === "function") {
onLoad(id);
}
};
const onErrorHandler = () => {
setHasError(true);
if (typeof onError === "function") {
onError(id);
}
};
return (
<img
src={src}
alt={alt}
width={width}
height={height}
onLoad={onLoadHandler}
onError={onErrorHandler}
/>
);
};
export default LoadableImage;
now you can handle the callbacks in your implementation and act appropriately. You can keep state of all your images, and their loading status.
App.js
export default function App() {
const [images, setImages] = useState(imageList);
const imagesLoading = images.some((img) => img.hasLoaded === false);
const handleImageLoaded = (id) => {
setImages((prevState) => {
const index = prevState.findIndex((img) => img.id === id);
const newState = [...prevState];
const newImage = { ...newState[index] };
newImage.hasLoaded = true;
newState[index] = newImage;
return newState;
});
};
return (
<div className="App">
{imagesLoading && <h2>Images are loading!</h2>}
{images.map((img) => (
<LoadableImage
key={img.id}
id={img.id}
src={img.src}
onLoad={handleImageLoaded}
/>
))}
</div>
);
}
Here handleImageLoaded will update the hasLoaded property of an image in the images state array when a image is loaded. You can then conditionally render your loading screen while (in this case) imagesLoading is true, as I have conditionally rended the "Imags are loading" text.
Codesandbox
imageList looks like this
const imageList = [
{
id: 1,
src:
"https://images.unsplash.com/photo-1516912481808-3406841bd33c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=683&q=80",
hasLoaded: false
},
{
id: 2,
src: "https://via.placeholder.com/150",
hasLoaded: false
},
{
id: 3,
src: "https://via.placeholder.com/151",
hasLoaded: false
}
];
I am striving to make customized rating component. I know there are other libraries for it but I wanted to do it myself so i can understand the underlying process. However, i am struggling on custom component part. For default case, it's working fine. For custom component what I tried is allow developer to pass svg component of their desired icon and then show that icon as a rating component. Up to this, it's working but I have no idea on handling mouseover, mouseout functionality.
here is what I have tried
import React from "react";
const DefaultComponent = ({
ratingRef,
currentRating,
handleMouseOut,
handleMouseOver,
handleStarClick,
totalRating
}) => {
return (
<div
className="rating"
ref={ratingRef}
data-rating={currentRating}
onMouseOut={handleMouseOut}
>
{[...Array(+totalRating).keys()].map(n => {
return (
<span
className="star"
key={n + 1}
data-value={n + 1}
onMouseOver={handleMouseOver}
onClick={handleStarClick}
>
★
</span>
);
})}
</div>
);
};
const CustomComponent = ({
ratingRef,
currentRating,
handleMouseOut,
handleMouseOver,
handleStarClick,
totalRating,
ratingComponent
}) => {
return (
<>
{[...Array(+totalRating).keys()].map(n => {
return ratingComponent({ key: n });
})}
</>
);
};
const Rating = props => {
const { totalRating = 5, onClick, ratingComponent } = props;
const [currentRating, setCurrentRating] = React.useState(0);
React.useEffect(() => {
handleMouseOut();
}, []);
const handleMouseOver = ev => {
const stars = ev.target.parentElement.getElementsByClassName("star");
const hoverValue = ev.target.dataset.value;
Array.from(stars).forEach(star => {
star.style.color = hoverValue >= star.dataset.value ? "#FDC60A" : "#444";
});
};
const handleMouseOut = ev => {
const stars = ratingRef?.current?.getElementsByClassName("star");
stars &&
Array.from(stars).forEach(star => {
star.style.color =
currentRating >= star.dataset.value ? "#FDC60A" : "#444";
});
};
const handleStarClick = ev => {
let rating = ev.target.dataset.value;
setCurrentRating(rating); // set state so the rating stays highlighted
if (onClick) {
onClick(rating); // emit the event up to the parent
}
};
const ratingRef = React.useRef();
console.log("ratingComponent", ratingComponent);
return (
<>
{ratingComponent ? (
<CustomComponent
ratingRef={ratingRef}
currentRating={currentRating}
handleMouseOut={handleMouseOut}
handleMouseOver={handleMouseOver}
handleStarClick={handleStarClick}
totalRating={totalRating}
ratingComponent={ratingComponent}
/>
) : (
<DefaultComponent
ratingRef={ratingRef}
currentRating={currentRating}
handleMouseOut={handleMouseOut}
handleMouseOver={handleMouseOver}
handleStarClick={handleStarClick}
totalRating={totalRating}
/>
)}
</>
);
};
export default Rating;
I have created a sandbox as well.
https://codesandbox.io/s/stoic-almeida-yz7ju?file=/src/components/Rating/Rating.js:0-2731
Can anyone give me idea on how to make reusable rating component which can support custom icons for rating like in my case i needed exactly that gem and star icon?
I'd do it this way:
reuse your DefaultComponent to take in the icon
use classnames instead of selecting DOM elements; one for default and one for hover. Add fill or color properties for the hover/selected classname
use a hoverIndex state to track which element I'm hovering on
Improvement: colocate the hover events within the DefaultComponent since the are relevant to only the icon, and aren't needed in the parent container
I had trouble styling the star svg. Essentially, you need to override the fill color for it - but it seems to be a complicated one. If you can simplify it, or pass down the fill as prop into the svg and use it internally - either would work!
Sandbox: https://codesandbox.io/s/epic-pascal-j1d0c
Hope this helps!
const DefaultIcon = _ => "a"; // = '★'
const DefaultComponent = ({
ratingComponent = DefaultIcon,
ratingRef,
currentRating,
handleStarClick,
totalRating
}) => {
const RatingIconComponent = ratingComponent;
const [hoverIndex, setHoverIndex] = React.useState(-Infinity);
const handleMouseOver = index => {
setHoverIndex(index);
};
const handleMouseOut = ev => {
setHoverIndex(-Infinity);
};
return (
<div
className="rating"
ref={ratingRef}
data-rating={currentRating}
onMouseOut={handleMouseOut}
>
{[...Array(+totalRating).keys()].map(n => {
const isFilled = n < currentRating || n <= hoverIndex;
return (
<span
className={`ratingIcon ${isFilled && "fill"}`}
key={n + 1}
data-value={n + 1}
onMouseOver={_ => handleMouseOver(n)}
onClick={_ => handleStarClick(n + 1)}
>
<RatingIconComponent />
</span>
);
})}
</div>
);
};
const Rating = props => {
const { totalRating = 5, onClick, ratingComponent } = props;
const [currentRating, setCurrentRating] = React.useState(0);
const handleStarClick = rating => {
setCurrentRating(rating); // set state so the rating stays highlighted
if (onClick) {
onClick(rating); // emit the event up to the parent
}
};
const ratingRef = React.useRef();
return (
<DefaultComponent
ratingComponent={ratingComponent}
ratingRef={ratingRef}
currentRating={currentRating}
handleStarClick={handleStarClick}
totalRating={totalRating}
/>
);
};
<Rating ratingComponent={Gem} />
/* for normal text */
.ratingIcon {
color: "gray";
}
.ratingIcon.fill {
color: yellow;
}
/* for svg */
.ratingIcon svg path {
fill: gray !important;
}
.ratingIcon.fill svg path {
fill: yellow !important;
}
Code is here: https://codesandbox.io/s/gatsby-starter-default-ry8sm
You can try demo: https://ry8sm.sse.codesandbox.io/
Every picture is an Enlarger component which will zoom in when you click on it. And they are designed to show up sequentially by fading in. I use Ref to track every Enlarger and here is the code snippet for it.
import Img from "react-image-enlarger"
class Enlarger extends React.Component {
state = { zoomed: false, opacity: 0 }
toggleOpacity = o => {
this.setState({ opacity: o })
}
render() {
const { index, orderIndex, src, enlargedSrc, onLoad } = this.props
return (
<div style={{ margin: "0.25rem" }} onLoad={onLoad}>
<Img
style={{
opacity: this.state.opacity,
transition: "opacity 0.5s cubic-bezier(0.25,0.46,0.45,0.94)",
transitionDelay: `${orderIndex * 0.07}s`,
}}
zoomed={this.state.zoomed}
src={src}
enlargedSrc={enlargedSrc}
onClick={() => {
this.setState({ zoomed: true })
}}
onRequestClose={() => {
this.setState({ zoomed: false })
}}
/>
</div>
)
}
}
export default Enlarger
And I have a Masonry component which will achieve the Masonry layout
import React, { Component } from "react"
import imagesLoaded from "imagesloaded"
import PropTypes from "prop-types"
import TransitionGroup from "react-transition-group/TransitionGroup"
class MasonryGrid extends Component {
componentDidMount() {
window.onload = this.resizeAllGridItems()
window.addEventListener("resize", this.resizeAllGridItems)
let allItems = document.getElementsByClassName("masonry-grid--item")
for (let x = 0; x < allItems.length; x++) {
imagesLoaded(allItems[x], this.resizeInstance)
}
}
resizeAllGridItems = () => {
let allItems = document.getElementsByClassName("masonry-grid--item")
for (let x = 0; x < allItems.length; x++) {
this.resizeGridItem(allItems[x])
}
}
resizeGridItem = item => {
let grid = document.getElementsByClassName("masonry-grid")[0]
let rowHeight = parseInt(
window.getComputedStyle(grid).getPropertyValue("grid-auto-rows")
)
let rowGap = parseInt(
window.getComputedStyle(grid).getPropertyValue("grid-row-gap")
)
let rowSpan = Math.ceil(
(item.querySelector(".content").getBoundingClientRect().height + rowGap) /
(rowHeight + rowGap)
)
item.style.gridRowEnd = "span " + rowSpan
}
resizeInstance = instance => {
let item = instance.elements[0]
this.resizeGridItem(item)
}
render() {
const MasonryGrid = {
display: "grid",
gridGap: `${this.props.gridGap}`,
gridTemplateColumns: `repeat(auto-fill, minmax(${
this.props.itemWidth
}px, 1fr))`,
gridAutoRows: "10px",
}
return (
<TransitionGroup>
<div className="masonry-grid" style={MasonryGrid}>
{this.props.children.length >= 1 &&
this.props.children.map((item, index) => {
return (
<div className="masonry-grid--item" key={index}>
<div className="content">{item}</div>
</div>
)
})}
</div>
</TransitionGroup>
)
}
}
MasonryGrid.defaultProps = {
itemWidth: 250,
gridGap: "6px 10px",
}
MasonryGrid.propTypes = {
itemWidth: PropTypes.number,
gridGap: PropTypes.string,
}
export default MasonryGrid
The problem is, if you look at the demo, when you click on tab project1, you will see the pictures show up on top of each other and doesn't spread well as intended. But once you resize the browser a little bit, they becomes normal and form the Masonry layout I wanted. I suspect it has something to do with the fade-in effect I implemented but I don't know how to fix it.
I am using draftjs editor. I could render the content but I could not show images. How can i show image when using draftjs? Right now the url is only shown instead of images.The server sends the data as following
img src="http://image_url" style="argin:30px auto; max-width: 350px;"
Sorry i could not use img tag html way so excluded the tag syntax.
function findImageEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(character => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === "IMAGE"
);
}, callback);
}
const Image = props => {
const { height, src, width } = props.contentState
.getEntity(props.entityKey)
.getData();
return <img src={src} height={height} width={width} />;
};
class AdminEditor extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
editorContent: undefined,
contentState: "",
touched: false
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.htmlMarkup !== this.props.htmlMarkup) {
const content = nextProps.htmlMarkup;
const blocksFromHTML = convertFromHTML(content);
const plainState = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap
);
this.setState(state => ({
editorState: EditorState.createWithContent(plainState, decorator)
}));
}
}
onEditorStateChange = editorState => {
this.setState({
editorState
});
};
onEditorChange = editorContent => {
this.setState({
editorContent
});
};
handleChange = event => {
this.props.setEditorState(
this.state.editorState.getCurrentContent().hasText() && this.state.touched
);
};
render() {
const { editorState } = this.state;
const { stateOfEditor } = this.props;
return (
<div>
<Editor
tabIndex={0}
editorState={editorState}
initialContentState={this.props.htmlMarkup}
toolbarClassName="home-toolbar"
onEditorStateChange={this.onEditorStateChange}
toolbar={{
history: { inDropdown: true },
inline: { inDropdown: false },
link: { showOpenOptionOnHover: true },
image: {
uploadCallback: this.imageUploadCallBack,
defaultSize: { height: "auto", width: "50%" }
}
}}
onContentStateChange={this.onEditorChange}
onChange={this.handleChange}
/>
</div>
);
}
}
export default AdminEditor;
exact copy of decorator is in top of the findImageEntities which i haven't pasted just to reduce the number of lines of code
I saw the props onEditorStateChange={this.onEditorStateChange} . I doubt you're using the draft-js-wysiwyg not draft-js.
In draft-js-wysiwyg , u can visit here :
https://github.com/jpuri/react-draft-wysiwyg/issues/589