Using ReactJS and GatsbyJS (v2) I have a header component that sits fixed over div id="outerContainer" in the parent layout component.
To toggle class on scroll position below 100px, I would generally use window.scrollY < 100.
However, due to the body class with the style overflow:hidden and outerContainer with overflow: scroll, the window scroll position does not change.
How can I define outerContainer.scrollY < 100 from a child component, whilst referencing the outerContainer of the parent for scroll position?
Layout.js
const Layout = ({ children }) => (
<div>
<div id="outerContainer" ref={el => (this.outerContainer = el)}>
<div className="content">{children}</div>
</div>
<Header />
</div>
);
Header.js
class Header extends React.Component {
constructor(props) {
super(props);
this.state = {
navScrolled: true
};
this.onScroll = this.onScroll.bind(this);
}
componentDidMount() {
document.addEventListener('scroll', () => {
const navScrolled = outerContainer.scrollY < 100;
if (navScrolled !== this.state.navScrolled) {
this.onScroll(navScrolled);
}
});
}
componentWillUnmount() {...}
...
}
My first thought is that I should perform function in layout.js and use props pass the state down. Is this the best option or perhaps is there another way that would keen everything in header.js?
Your first thought is correct, parent class is proper place for your onScroll callback, because the scroll position is a property of your entire layout, not of header which only uses it.
Also, by keeping onScroll in parent class, you keep yourself able to pass it to outerContainer as well, if it grows into separate class during the development, which is likely to happen.
Related
My goal is to turn this class component into a function component.
The issue, however, with my Functional Component Implementation is that when I scroll up, the header is instantly expanded. I only want the header visible if the page is at the absolute top.
import React, { useState, useEffect } from "react";
import classnames from "classnames";
import "./header.styles.scss";
const Header = () => {
const [isHidden, setIsHidden] = useState(0);
const [prevScrollPos, setPrevScollPos] = useState(window.pageYOffset);
useEffect(() => {
function handleScrollChange() {
setIsHidden(prevScrollPos < window.pageYOffset);
setPrevScollPos(window.pageYOffset);
}
window.addEventListener("scroll", handleScrollChange);
return () => {
window.removeEventListener("scroll", handleScrollChange);
};
});
return (
<div
className={classnames("header", {
"header-hidden": isHidden,
})}
>
<div className="logo-container">A & A Solutions</div>
<div className="options">
<div className="option">Option 1</div>
<div className="option">Option 2</div>
<div className="option">Option 3</div>
</div>
</div>
);
};
export default Header;
You could check the window.scrollY property and see if it is at 0. This means the top of the window's scrollbar's Y-axis at the very top.
function handleScrollChange() {
setIsHidden(window.scrollY !== 0);
}
As a side note, I don't think you should add and clean up your event listeners every time a side effect occurs. You could probably look into using the [] dependency for your useEffect since you can omit the checking of prevScrollPos
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run.
From the docs of react portal:
A typical use case for portals is when a parent component has an overflow: hidden or z-index style, but you need the child to visually “break out” of its container. For example, dialogs, hovercards, and tooltips.
The suggested solution is to use:
// this line is rendered in `Portal1` component,
// which is rendered in `Parent1` component.
ReactDOM.createPortal(Child1, Container1)
I don't understand what does it solves. Why making Child1 child of Container1 instead of Parent1 helps?
My question maybe not clear so if it doesn't -> How does this solution differ from other solutions for creating "dialogs, hovercards, and tooltips"?
When you initialise a React application, ReactDOM tells one DOM container that all its React components will be rendered under this DOM. This makes React do all rendering processing.
Sometimes you need to control a React Component to render as a child to a different DOM element, and continue to interact with your React application. This is why we use React Portals
As React creates virtual elements under the hood, you cannot convert the into DOM elements and insert them directly into the DOM. React Portals allows to you pass a React Elements and specify the container DOM for the React Element
Here is an example:
You have a Modal component which renders a div element in the center.
function Modal() {
return (
<div style={{ position: 'absolute', left: '50%'}}>
Message
</div>
);
}
One puts your Modal component inside a div of relative position.
<div style={{ position: 'relative', left: 100 }}>
<Modal />
</div>
The problem is when Modal component is rendered, its position is relative to parent div's position but you need to show it at the centre of window.
In order to solve this problem, you can append your Modal component directly to the body element with a portal
Here is the solution with Portals.
function ModalRenderer() {
return (
React.createPortal(
<Modal />,
document.body
)
);
}
And use ModalRenderer component anywhere inside your application.
<div style={{ position: 'relative', left: 100 }}>
<ModalRenderer />
</div>
ModalRenderer has the container element for the Modal which is outside of the DOM tree, but still within the React Application tree
In React V15,we can only add children dom into the father dom.That means, if you want to have an element, you have to create a new div.Like this:
<div>
{this.props.children}
</div>
In React v16,don't need to create a new div.We can use portal to add the children element to any dom in the dom tree.
ReactDOM.createPortal(
this.props.children,
domNode
);
overflow: hidden or z-index style
If a parent component has an overflow: hidden or z-index style, and the children element type is dialogs, hovercards, tooltips and so on,these should be on the upper layer of the father element, meaning break out.But they maybe shade by the father component.
So createPortal offers a better option.It can load on the upper component of the father component.After mounting the element to another dom,it won't be sheltered.
Event and bubble up
Even the component mounted on another component, event can budde up to the father component.
One good case is to separate CSS concerns.
Here is an example:
HTML
<div id="app-root"></div>
<div id="modal-root"></div>
CSS
.app {
position: fixed;
height: 100%;
width: 100%;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.modal {
background-color: rgba(0,0,0,0.5);
}
Babel
const appRoot = document.getElementById('app-root')
const modalRoot = document.getElementById('modal-root')
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
showModal: false
}
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
}
handleShow() {
this.setState({
showModal: true
})
}
handleHide() {
this.setState({
showModal: false
})
}
render() {
const modal = this.state.showModal ? (
<Modal>
<div className="modal">I am no longer centered!</div>
</Modal>
) : null
return (
<div className='app'>
Basic
<button onClick={this.handleShow}>
Show Modal
</button>
{modal}
</div>
)
}
}
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount(){
modalRoot.appendChild(this.el)
}
componentWillUnmount() {
modalRoot.removeChild(this.el)
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el
)
}
}
ReactDOM.render(<App />, appRoot)
Observations:
1. The main text has fixed position and is centered
2. When we click the button, the text popped up. Notice the text is no longer centered!
3. If we had used it to something like this:
<div className='app'>
Basic
<button onClick={this.handleShow}>
Show Modal
</button>
<div className="modal">I am centered ;(</div>
</div>
Notice the text is centered. Modal works by going to modal-root and attach that DOM element there. You can have your own CSS under modal-root hood, separate from parent component (app-root).
You are now no longer obligated to attach your "child" components under your parent's. In this case, you (app-root) are attaching it to its sibling (modal-root). You can totally attach it to document.body, or whatever element you wanted to. Another perk is, like other user mentioned, event bubbling happens as if that child component is their own child.
A Parent component in #app-root would be able to catch an uncaught, bubbling event from the sibling node #modal-root.
Source
I'm using react, and I would like to display a header only after the user has scrolled down 150px on the page.
So when the user starts to scroll down, I would like a sticky header to appear. And again, when the user scrolls to the top of the page, the sticky header disappears.
<div className="container">
// display this on top of the page:
<JumbotronImage />
// display this when user starts to scroll, sticky on top of the page
<StickyHeader />
</div>
I tried to do it with window.addEventListener('scroll'... , and I also tried https://github.com/fisshy/react-scroll but couldn't get it to work yet. Any suggestions?
I can think upon that your code would look as following. And the solution should fit like this.
export class App extends React.Component {
componentDidMount() {
ReactDOM.findDOMNode(this.stickyHeader).addEventListener('scroll', this.handleScroll.bind(this));
}
componentWillUnmount() {
ReactDOM.findDOMNode(this.stickyHeader).removeEventListener('scroll', this.handleScroll.bind(this));
}
handleScroll() {
// Add the logic here
}
render() {
const {props} = this;
return (
<div className="container">
// display this on top of the page:
<JumbotronImage />
// display this when user starts to scroll, sticky on top of the page
<StickyHeader ref = {ele => this.stickyHeader = ele} />
</div>
);
}
}
You can refer this article which worked for me, http://blog.sodhanalibrary.com/2016/07/detect-scroll-direction-using-reactjs.html#.Wo0hq0nTS-4 to add logic inside handleScroll.
I generally use this script for sticky header for my header with id="sticky"
$(window).scroll(function () {
var iCurScrollPos = $(this).scrollTop();
if (iCurScrollPos) {
//#sticky put your element for which you want it to apply
$('#sticky').addClass('stickyMenu');
} else {
$('#sticky').removeClass('stickyMenu');
}
});
Add css for attached class
I have a series of questions for a sign-up flow I am building. Currently, I am looping through each components and displaying them all on one page. My questions are, How do I show only one at a time? How can I include a slide left transition/animation when each slide hides/shows? I would like each question to display individually then once the user clicks next, it hides the first question and displays the second. I am a bit newer to React so I apologize if this is a basic question but I cannot figure it out.
Below are breakouts of my code:
import React from 'react';
import Q1Name from './questions/Q1Name';
import Q2Birthday from './questions/Q2Birthday';
import Q3City from './questions/Q3City';
import Q4YouReady from './questions/Q4YouReady';
import Q5Setting from './questions/Q5Setting';
import Q6Length from './questions/Q6Length';
import Q7Email from './questions/Q7Email';
class SignUpPage extends React.Component {
render() {
const components = [Q1Name, Q2Birthday, Q3City, Q5Setting, Q6Length, Q7Email];
const componentsToRender = components.map((Component, i) => (
<Component key={i} />
));
return (
<div className = "container-fluid">
<div className = "question-box">
{componentsToRender}
</div>
</div>
);
}
}
export default SignUpPage;
This is an example component - they are all slightly different so I am showing the two primary types:
the first only has a single "next button"
import React from 'react';
class Q2Birthday extends React.Component {
render() {
return (
<div className="questions">
<h1 id="question-h1">When is your birthday?</h1>
<form>
<div className="form-group">
<input type="date" className="form-control custom-form" id="birthdayInput" aria-describedby="birthday" placeholder="" />
</div>
<button type="submit" className="btn btn-custom btn-lg">Next Question!</button>
</form>
</div>
);
}
}
export default Q2Birthday;
the second has 3 button options the user can select from
import React from 'react';
class Q6Length extends React.Component {
render() {
return (
<div className="questions">
<h1 id="question-h1">How long would you like your trip to be?</h1>
<button type="submit" className="btn btn-custom-select btn-lg">Just a weekend!</button>
<button type="submit" className="btn btn-custom-select btn-lg">A full week!</button>
<button type="submit" className="btn btn-custom-select btn-lg">I'm flexible!</button>
</div>
);
}
}
export default Q6Length;
I also want to add in a slide left transition for the "questions" div within the question-box class. I have been reading up on react-transition-group but I am a bit confused on how to implement it. Also, with this application, I do not need to store the values of the form data.
How do I show only one at a time?
Given that you want to do a slide transition between them, you need at least the one being left behind and the next one to show to be in the DOM at switchover time. When not switching it's possible to have only the current one in the DOM. But simplest would probably be to always have all of them in the DOM, just with the previous/next ones out of the left/right sides of the viewport.
So to answer the question of how to show just one at a time, one way would be to translate all "old" ones left by 100% of the container width, leave the current one be, and translate all "next" ones right by 100%.
Styles for that might look like this:
const oldStyle = {
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
transform: 'translate(-100%)',
};
const currentStyle = {
position: 'relative',
transform: 'translate(0)',
};
const nextStyle = {
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
transform: 'translate(100%)',
};
Depending on where this is on your page, you might need some additional styles to hide the overflowing next/previous slides. Look up the overflow property. You may also need to fix heights or widths of the container -- look into this if you find the non-current slides have an unexpected (such as zero) height or width.
To apply the appropriate styles to each panel, you'll need to know which is which.
I'd suggest keeping track of the current slide index in your parent component's state. Say you have this in this.state.currentSlide. With that, you can choose the slide styles like this:
const componentsToRender = components.map((Component, i) => (
<Component key={i} style={i < this.state.currentSlide ? oldStyle : i === this.state.currentSlide ? currentStyle : nextStyle} />
));
In order for that style prop to pass through to your slides, you'd need to tweak the slides a little. The simplest way would just be to explicitly pass that one through:
<div className="questions" style={this.props.style}>
But how do we set the current slide in state, and keep it up to date? Well, in the simplest case, you need to initialize it at zero. Your slide components will need to tell the parent when they've finished. And you'll need to notice that, and update the state.
class SignUpPage extends React.Component {
constructor(props) {
super(props);
// Set initial state
this.state = {
currentSlide: 0,
};
}
// Handle notification from a child slide that we should move to the next
nextSlide() {
this.setState({
currentSlide: this.state.currentSlide + 1,
});
}
render() {
...
const componentsToRender = components.map((Component, i) => (
<Component key={i} style={this.props.style} onNext={this.nextSlide.bind(this)} />
));
The child components then need to call this method which has been passed in when they're finished:
class Q2Birthday extends React.Component {
handleSubmit(event) {
// Don't perform an actual form submission
event.preventDefault();
// Notify the parent
this.props.onNext();
}
render() {
return (
<div className="questions" style={this.props.style}>
<h1 id="question-h1">When is your birthday?</h1>
<form onSubmit={this.handleSubmit}>
...
How can I include a slide left transition/animation when each slide hides/shows?
With the above styles, it might be as simple as setting the following style for each of the slides:
transition: transform 0.5s;
I am trying to implement a List view in React.
What I am trying to achieve is that to store the list headers informations and register the components and register the scroll event.
every time when user scroll the window, I'd like to take out the stored div and re-calculate the offsetTop data.
The problem now is that, I found the console just print out the initial value (the value is fixed and never changed) offsetTop data never change in onscroll function.
Anyone suggest how to get latest offsetTop from the _instances object?
import React, { Component } from 'react';
import ListHeader from './lib/ListHeader';
import ListItems from './lib/ListItems';
const styles = {
'height': '400px',
'overflowY': 'auto',
'outline': '1px dashed red',
'width': '40%'
};
class HeaderPosInfo {
constructor(headerObj, originalPosition, originalHeight) {
this.headerObj = headerObj;
this.originalPosition = originalPosition;
this.originalHeight = originalHeight;
}
}
export default class ReactListView extends Component {
static defaultProps = {
events: ['scroll', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll', 'resize', 'touchmove', 'touchend'],
_instances:[],
_positionMap: new Set(),
_topPos:'',
_topWrapper:''
}
static propTypes = {
data: React.PropTypes.array.isRequired,
headerAttName: React.PropTypes.string.isRequired,
itemsAttName: React.PropTypes.string.isRequired,
events: React.PropTypes.array,
_instances: React.PropTypes.array,
_positionMap: React.PropTypes.object,
_topPos: React.PropTypes.string,
_topWrapper: React.PropTypes.object
};
state = {
events: this.props.events,
_instances: this.props._instances,
_positionMap: this.props._positionMap,
_topPos: this.props._topPos
}
componentDidMount() {
this.initStickyHeaders();
}
componentWillUnmount() {
}
componentDidUpdate() {
}
refsToArray(ctx, prefix){
let results = [];
for (let i=0;;i++){
let ref = ctx.refs[prefix + '-' + String(i)];
if (ref) results.push(ref);
else return results;
}
}
initHeaderPositions() {
// Retrieve all instance of headers and store position info
this.props._instances.forEach((k)=>{
this.props._positionMap.add(new HeaderPosInfo(
k,
k.refs.header.getDOMNode().offsetTop,
k.refs.header.getDOMNode().offsetHeight
));
});
let it = this.props._positionMap.values();
let first = it.next();
this.props._topPos = first.value.originalPosition;
this.props._topWrapper = first.value.headerObj;
}
initStickyHeaders () {
this.props._instances = this.refsToArray(this, 'ListHeader');
this.initHeaderPositions();
// Register events listeners with the listview div
this.props.events.forEach(type => {
if (window.addEventListener) {
React.findDOMNode(this.refs.listview).addEventListener(type, this.onScroll.bind(this), false);
} else {
React.findDOMNode(this.refs.listview).attachEvent('on' + type, this.onScroll.bind(this), false);
}
});
}
onScroll() {
// update current header positions and apply fixed positions to the top one
console.log(1);
let offsetTop = React.findDOMNode(this.props._instances[0].refs.header).offsetTop;
}
render() {
const { data, headerAttName, itemsAttName } = this.props;
let _refi = 0;
let makeRef = () => {
return 'ListHeader-' + (_refi++);
};
return (
<div ref="listview" style={styles}>
{
Object.keys(data).map(k => {
const header = data[k][headerAttName];
const items = data[k][itemsAttName];
return (
<ul key={k}>
<ListHeader ref={makeRef()} header={header} />
<ListItems items={items} />
</ul>
);
})
}
</div>
);
}
}
The whole source code is on Github, you can clone and compile it from here:
Github
You may be encouraged to use the Element.getBoundingClientRect() method to get the top offset of your element. This method provides the full offset values (left, top, right, bottom, width, height) of your element in the viewport.
Check the John Resig's post describing how helpful this method is.
I do realize that the author asks question in relation to a class-based component, however I think it's worth mentioning that as of React 16.8.0 (February 6, 2019) you can take advantage of hooks in function-based components.
Example code:
import { useRef } from 'react'
function Component() {
const inputRef = useRef()
return (
<input ref={inputRef} />
<div
onScroll={() => {
const { offsetTop } = inputRef.current
...
}}
>
)
}
Eugene's answer uses the correct function to get the data, but for posterity I'd like to spell out exactly how to use it in React v0.14+ (according to this answer):
import ReactDOM from 'react-dom';
//...
componentDidMount() {
var rect = ReactDOM.findDOMNode(this)
.getBoundingClientRect()
}
Is working for me perfectly, and I'm using the data to scroll to the top of the new component that just mounted.
A quicker way if you are using React 16.3 and above is by creating a ref in the constructor, then attaching it to the component you wish to use with as shown below.
...
constructor(props){
...
//create a ref
this.someRefName = React.createRef();
}
onScroll(){
let offsetTop = this.someRefName.current.offsetTop;
}
render(){
...
<Component ref={this.someRefName} />
}
import ReactDOM from 'react-dom';
//...
componentDidMount() {
var n = ReactDOM.findDOMNode(this);
console.log(n.offsetTop);
}
You can just grab the offsetTop from the Node.
A better solution with ref to avoid findDOMNode that is discouraged.
...
onScroll() {
let offsetTop = this.instance.getBoundingClientRect().top;
}
...
render() {
...
<Component ref={(el) => this.instance = el } />
...
onScroll has a events whict contains all the native and child elements inside this div so u can use it like this shown below and get the targetted element offsetTop.
const getoffSet = e => {
console.log(e, e.natiiveEvent.target.childNodes[0].offsetTop)
}
return (
<div onScroll={(e) => getoffSet(e)} ref={listview} style={styles}>
</div>
)
Checking if height Property Is Not Set on Parent:
If the parent element has no height set then the sticky element won't
have any area to stick to when scrolling. This happens because the
sticky element is meant to stick/scroll within the height of a
container.
Checking if a Parent Element Is a Flexbox
If sticky element's parent is a flexbox, there are two scenarios to
check for:
The sticky element has align-self: auto set (which is the default);
The sticky element has align-self: stretch set. If the Sticky Element
Has align-self: auto Set: In this case the value of align-self would
compute to the parent's align-items value. So,
if the parent has align-items: normal (which is the default) or
align-items: stretch set, then it means the height of the sticky element would stretch to fill the entire available space. This would leave no room for the sticky element to scroll within the parent.
If the Sticky Element Has align-self: stretch Set:
In this case, the sticky element would stretch to the height of the parent, and would not have any area to scroll within.
How to Make Sticky Element Scrollable Within a Flexbox:
You could simply set the value of the align-self property to align-self: flex-start. This would put the sticky element at the start and won't stretch it.enter link description here