I'm using the https://github.com/locomotivemtl/locomotive-scroll plugin in react to enable smooth scrolling. I'm trying to get it to do a Continuous scroll so it will loop round no matter what direction you scroll in by adding and removing items in the state array to change the order.
This works but causes some super flicker.
coulep of demos is
https://codesandbox.io/s/heuristic-resonance-0ybkl
https://codesandbox.io/s/young-wind-ozeeq
https://codesandbox.io/s/patient-darkness-6gxj1
Any help would be appreciated
code below
import React from "react";
import ReactDOM from "react-dom";
import LocomotiveScroll from "locomotive-scroll";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
limit: null,
items: [
"https://www.fillmurray.com/640/460",
"https://www.fillmurray.com/g/640/560",
"https://www.fillmurray.com/640/760",
"https://www.fillmurray.com/g/640/860",
"https://www.fillmurray.com/640/160",
"https://www.fillmurray.com/g/640/260",
"https://www.fillmurray.com/g/640/360",
"https://www.fillmurray.com/g/640/460"
]
};
}
componentDidMount() {
const target = document.querySelector("#view");
const scroll = new LocomotiveScroll({
el: target,
smooth: true,
smoothMobile: true,
getDirection: true
});
scroll.on("scroll", obj => {
console.log(obj.scroll.y);
if (this.state.limit !== null) {
if (obj.direction === "up") {
var current = this.state.items;
var first = current.shift();
current.push(first);
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
}
if (obj.direction === "down") {
current = this.state.items;
var last = current.pop();
current.unshift(last);
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
}
} else {
var halfway = obj.limit / 2;
scroll.scrollTo(target, halfway);
this.setState({
limit: obj.limit
});
}
});
scroll.scrollTo(target, 1);
}
render() {
return (
<div className="App">
<div id="view">
<div className="left">
{this.state.items.map(item => {
return <img src={item} alt="" />;
})}
</div>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The reason you are getting flickering is because the entire component is re-rendering on the scroll event and your elements don't have a key on them.
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
In the above code, you set the state on a scroll event which will cause a full re-render of the list. You need to set a key on your elements that are in a map func.
Key docs
Code Sandbox
import React from "react";
import ReactDOM from "react-dom";
import LocomotiveScroll from "locomotive-scroll";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
limit: null,
items: [
"https://www.fillmurray.com/640/460",
"https://www.fillmurray.com/g/640/560",
"https://www.fillmurray.com/640/760",
"https://www.fillmurray.com/g/640/860",
"https://www.fillmurray.com/640/160",
"https://www.fillmurray.com/g/640/260",
"https://www.fillmurray.com/g/640/360",
"https://www.fillmurray.com/g/640/460"
]
};
}
componentDidMount() {
const target = document.querySelector("#view");
const scroll = new LocomotiveScroll({
el: target,
smooth: true,
smoothMobile: true,
getDirection: true
});
scroll.on("scroll", ({ limit, direction }) => {
if (this.state.limit !== null) {
if (direction === "up") {
console.log("up");
var current = this.state.items;
var last = current.pop();
current.unshift(last);
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
}
if (direction === "down") {
current = this.state.items;
var first = current.shift();
current.push(first);
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
}
} else {
var halfway = limit / 2;
scroll.scrollTo(target, halfway);
this.setState({
limit
});
}
});
scroll.scrollTo(target, 1);
}
render() {
return (
<div className="App">
<div id="view">
<div className="left">
{this.state.items.map((item, idx) => {
return <img key={`${item}${idx}`} src={item} alt="" />;
})}
</div>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Related
How can I move one or more const out of function App?
In the simple test App below, I'm using localStorage to store a value which determines if a div is dispayed. The handleToggle dismisses the div and stores a value in localStorage. Clearing localstorage and reloading shows the div again.
In a simple test App on localhost, this works. But in my more complex production App, I'm getting the error Invalid hook call. Hooks can only be called inside of the body of a function component , which has a myriad of fixes, one of which points out the issue may be that a const needs to be a separate function.
And so I'm thinking the issue is that I need to convert the two const to a function that can be placed right under the import blocks and out of the function App() block.
As a start, in this simple App, how can I move the two const out of function App()?
import './App.css';
import * as React from 'react';
function App() {
const [isOpen, setOpen] = React.useState(
JSON.parse(localStorage.getItem('is-open')) || false
);
const handleToggle = () => {
localStorage.setItem('is-open', JSON.stringify(!isOpen));
setOpen(!isOpen);
};
return (
<div className="App">
<header className="App-header">
<div>{!isOpen && <div>Content <button onClick={handleToggle}>Toggle</button></div>}</div>
</header>
</div>
);
}
export default App;
Edit: This is the full production file with Reza Zare's fix that now throws the error 'import' and 'export' may only appear at the top level on line 65:
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import {
Compose,
Notifications,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
HashtagTimeline,
DirectTimeline,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Directory,
} from '../../ui/util/async-components';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses,
'BOOKMARKS': BookmarkedStatuses,
'LIST': ListTimeline,
'DIRECTORY': Directory,
};
// Added const
const getInitialIsOpen = () => JSON.parse(localStorage.getItem('is-open')) || false;
const App = () => {
const [isOpen, setOpen] = React.useState(getInitialIsOpen());
const handleToggle = () => {
localStorage.setItem('is-open', JSON.stringify(!isOpen));
setOpen(!isOpen);
};
function getWeekNumber(d) {
d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7));
var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
var weekNo = Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7);
return [d.getUTCFullYear(), weekNo];
}
var result = getWeekNumber(new Date());
// errors out here: 'import' and 'export' may only appear at the top level.
export default class ColumnsArea extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = {
columns: ImmutablePropTypes.list.isRequired,
isModalOpen: PropTypes.bool.isRequired,
singleColumn: PropTypes.bool,
children: PropTypes.node,
};
// Corresponds to (max-width: $no-gap-breakpoint + 285px - 1px) in SCSS
mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)');
state = {
renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
}
componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
if (this.mediaQuery) {
if (this.mediaQuery.addEventListener) {
this.mediaQuery.addEventListener('change', this.handleLayoutChange);
} else {
this.mediaQuery.addListener(this.handleLayoutChange);
}
this.setState({ renderComposePanel: !this.mediaQuery.matches });
}
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
}
componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
}
componentWillUnmount () {
if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
if (this.mediaQuery) {
if (this.mediaQuery.removeEventListener) {
this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
} else {
this.mediaQuery.removeListener(this.handleLayouteChange);
}
}
}
handleChildrenContentChange() {
if (!this.props.singleColumn) {
const modifier = this.isRtlLayout ? -1 : 1;
this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}
handleLayoutChange = (e) => {
this.setState({ renderComposePanel: !e.matches });
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
}
setRef = (node) => {
this.node = node;
}
renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
}
renderError = (props) => {
return <BundleColumnError multiColumn errorType='network' {...props} />;
}
render () {
const { columns, children, singleColumn, isModalOpen } = this.props;
const { renderComposePanel } = this.state;
if (singleColumn) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
</div>
</div>
<div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' />
// output of getInitialIsOpen
<div class='banner'>
{!isOpen && <div>Content <button onClick={handleToggle}>Toggle</button></div>}
</div>
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
</div>
);
}
return (
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
{columns.map(column => {
const params = column.get('params', null) === null ? null : column.get('params').toJS();
const other = params && params.other ? params.other : {};
return (
<BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
</BundleContainer>
);
})}
{React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
</div>
);
}
}
I've inherited a react/node/prismic application which has a ScrollToTop.js file to make sure pages load at the top, which they do when accessed from our main navigation menu.
This application also has a little side nav that's hidden until you scroll down (controlled with tabIndex).
The bug: when you click a link from the side nav, the resulting page comes up however far down you had scrolled when opening the side nav. Instead, I want these to start at the top.
We have a Layout.js file for the overall layout, and a specific SideNav.js for that little side nav. I'm new to react/javascript, and I haven't been able to figure out how to either (a) apply the ScrollToTop logic to these sidenav links or (b) add an additional window.scrollTo(0,0) for this special case. Can anyone recommend how/where this can be updated?
SideNav.js:
import React from 'react'
import CTAButton from './CTAButton'
import { Link } from 'react-router-dom'
import { Link as PrismicLink } from 'prismic-reactjs'
import PrismicConfig from '../../../prismic-configuration'
import PropTypes from 'prop-types'
import * as PrismicTypes from '../Utils/Types'
const matchPaths = {
'about': 'About us',
'projects': 'Projects',
'news': 'News'
}
class SideNav extends React.Component {
constructor(props) {
super(props)
this.state = {
expanded: false
}
this.handleKey = this.handleKey.bind(this)
this.expandMenu = this.expandMenu.bind(this)
this.keypressed = undefined
this.main = undefined
this.bug = undefined
this.toggle = false
this.windowTop = 0
}
checkMobile() {
if (window.innerWidth <= 800) {
this.setState({scrollThreshold: 50})
}
}
componentDidMount() {
this.checkMobile()
this.keypressed = this.handleKey
window.addEventListener('keydown', this.keypressed)
this.main = document.getElementsByClassName('top-nav-logo')[0]
this.bug = document.getElementsByClassName('side-nav-bug-wrap')[0]
}
componentWillUnMount() {
window.removeEventListener('keydown', this.keypressed)
}
handleKey(e) {
if (e.keyCode === 27 && this.state.expanded) {
this.expandMenu()
}
}
expandMenu() {
const el = document.scrollingElement || document.documentElement
if (!this.state.expanded) {
this.windowTop = el.scrollTop
} else {
el.scrollTop = this.windowTop
}
this.setState({
expanded: !this.state.expanded
})
document.body.classList.toggle('nav-lock')
}
render() {
const expanded = this.state.expanded ? 'expanded' : 'contracted'
const tabIndex = this.state.expanded ? '1' : '-1'
if (this.state.scrollThreshold === 50 && this.state.expanded) {
this.props.removeListener()
this.main.setAttribute('style', 'display:none')
this.bug.setAttribute('style', 'display:none; position:absolute')
}
else if (this.state.scrollThreshold === 50 && !this.state.expanded) {
this.props.addListener()
this.main.setAttribute('style', 'display:inline')
this.bug.setAttribute('style', 'display:flex; position:fixed')
}
const menu = this.props.menu.map((menuItem, index) => {
const menuLink = PrismicLink.url(menuItem.link, PrismicConfig.linkResolver)
const label = menuItem.label[0].text
let marker
if (typeof this.props.location !== 'undefined') {
// Match label to window location to move indicator dot
marker = label === matchPaths[this.props.location] ? this.props.location : 'inactive'
}
return (
<li key={index} className="side-nav-li">
<Link to={label} className="side-nav-link" onClick={this.expandMenu} tabIndex={tabIndex}>{label}</Link>
<div className={`side-nav-marker ${marker}`}/>
</li>
)
})
return (
<div className='side-nav-wrapper'>
<Link to='/' className={`side-nav-logo-main ${this.props.visibility}`} alt=""/>
<div className={`side-nav-bug-wrap ${this.props.visibility}`} onClick={this.expandMenu}>
<div className='side-nav-bug-icon' />
</div>
<div className={`side-nav ${expanded}`}>
<div className={'side-nav-menu-wrap'}>
<div className="side-nav-control-wrap">
<Link to="/" className="side-nav-logo" alt="Count Me In logo" onClick={this.expandMenu} tabIndex={tabIndex}/>
<button className="side-nav-exit" onClick={this.expandMenu} tabIndex={tabIndex}/>
</div>
<nav>
<ul className="side-nav-menu-items">
{ menu }
<CTAButton />
</ul>
</nav>
</div>
</div>
<div className={`side-nav-overlay ${expanded}`} onClick={this.expandMenu}/>
</div>
)
}
}
SideNav.propTypes = {
removeListener: PropTypes.func,
addListener: PropTypes.func,
location: PropTypes.string,
visibility: PropTypes.string,
menu: PropTypes.arrayOf(PropTypes.shape({
label: PrismicTypes.PrismicTextTypes,
link: PropTypes.shape({
id: PropTypes.string,
isBroken: PropTypes.bool,
lang: PropTypes.string,
link_type: PropTypes.string,
slug: PropTypes.string,
tags: PropTypes.array,
type: PropTypes.string,
uid: PropTypes.string
})
}))
}
export default SideNav
Layout.js:
const matchPaths = {
'about': 'About us',
'projects': 'Projects',
'news': 'News'
}
class Layout extends React.Component {
constructor(props) {
super(props)
this.state = {
location: undefined,
displayMobile: false,
visibility: 'offscreen',
scrollThreshold: 100
}
this.onMobileClick = this.onMobileClick.bind(this)
this.logoClick = this.logoClick.bind(this)
this.scrollCheck = this.scrollCheck.bind(this)
this.addScrollCheck = this.addScrollCheck.bind(this)
this.removeScrollCheck = this.removeScrollCheck.bind(this)
this.addChildScroll = this.addChildScroll.bind(this)
this.removeChildScroll = this.removeChildScroll.bind(this)
this.checkCount = 0
this.childCheckCount = 0
this.scrollCheckToggle = true
this.childCheckToggle = true
}
getBodyScrollTop () {
const el = document.scrollingElement || document.documentElement
return el.scrollTop
}
// Need to do this on didMount so window object is available.
componentDidMount() {
const loc = window.location.pathname.substr(1)
this.setState({ location: loc })
//Hide the loader and display the site content.
setTimeout(function() {
document.body.classList.add('is-loaded')
}, 50)
this.addScrollCheck()
if (this.getBodyScrollTop() > this.state.scrollThreshold) {
this.setState({
visibility: 'onscreen'
})
}
}
addChildScroll() {
if (this.childCheckCount < 1 ) {
window.addEventListener('scroll', this.scrollCheck)
this.childCheckCount++
this.childCheckToggle = true
}
}
removeChildScroll() {
if (this.childCheckCount === 1) {
window.removeEventListener('scroll', this.scrollCheck)
this.childCheckCount--
this.childCheckToggle = false
}
}
addScrollCheck() {
if (this.checkCount < 1 ) {
window.addEventListener('scroll', this.scrollCheck)
this.checkCount++
this.scrollCheckToggle = true
}
}
removeScrollCheck() {
if (this.checkCount === 1) {
window.removeEventListener('scroll', this.scrollCheck)
this.checkCount--
this.scrollCheckToggle = false
}
}
componentWillUnMount() {
this.removeScrollCheck()
}
scrollCheck() {
const scrollPos = this.getBodyScrollTop()
const newVis = scrollPos > this.state.scrollThreshold ? 'onscreen' : 'offscreen'
const curVis = this.state.visibility
newVis !== curVis && (
this.setState({
visibility: scrollPos > this.state.scrollThreshold ? 'onscreen' : 'offscreen'
})
)
}
// Need to do it again on didUpdate to handle nav updates
componentDidUpdate() {
let loc = window.location.pathname
loc = loc.substr(1)
if (loc !== this.state.location) {
this.setState({ location: loc })
}
}
// this class assignment sets all the mobile menu style changes & transitions
onMobileClick() {
// but we only want to do this setting on mobile
if (window.innerWidth < 800) {
this.scrollCheckToggle ? this.removeScrollCheck() : this.addScrollCheck()
this.setState({
displayMobile: this.state.displayMobile === true ? false : true
})
const appContainer = document.getElementsByClassName('app-container')[0]
appContainer.classList.toggle('locked')
}
}
logoClick() {
if (this.state.displayMobile === true) {
this.setState({displayMobile: false})
const appContainer = document.getElementsByClassName('app-container')[0]
appContainer.classList.toggle('locked')
}
}
render() {
const mobileDisplay = this.state.displayMobile === true ? '-active' : '-inactive'
const menu = this.props.menu.map((menuItem, index) => {
const menuLink = PrismicLink.url(menuItem.link, PrismicConfig.linkResolver)
const label = menuItem.label[0].text
let marker
if (typeof this.state.location !== 'undefined') {
// Match label to window location to move indicator dot
marker = label === matchPaths[this.state.location] ? this.state.location : 'inactive'
}
return (
<li key={index} className="top-nav-li">
<Link to={label} className="top-nav-link" onClick={this.onMobileClick}>{label}</Link>
<div className={`top-nav-marker ${marker}`} />
</li>
)
})
return (
<div className="app-container">
<Loader />
<SideNav menu={this.props.menu} location={this.state.location} visibility={this.state.visibility} addListener={this.addChildScroll} removeListener={this.removeChildScroll}/>
<header className='top-nav-container CONSTRAIN'>
<Link to="/" className={`top-nav-logo ${this.state.visibility}`} onClick={this.logoClick} alt="Count Me In logo"/>
<div className="top-nav-mobile-wrapper">
<div className={'top-nav-mobile-title'} onClick={this.onMobileClick} tabIndex="0">
<span className={`top-nav-mobile-title-text${mobileDisplay}`}>Menu</span>
<div className={`top-nav-mobile-icon${mobileDisplay}`} />
</div>
</div>
<nav className={`top-nav-menu-container${mobileDisplay}`}>
<ul className="top-nav-ul">
{menu}
<CTAButton/>
</ul>
</nav>
</header>
<main>
{this.props.children}
</main>
<Footer projects={this.props.projects} footerData={this.props.footerData} />
</div>
)
}
}
Layout.propTypes = {
children: PropTypes.node,
menu: PropTypes.arrayOf(PropTypes.shape({
label: PrismicTypes.PrismicTextTypes,
link: PropTypes.shape({
id: PropTypes.string,
isBroken: PropTypes.bool,
lang: PropTypes.string,
link_type: PropTypes.string,
slug: PropTypes.string,
tags: PropTypes.array,
type: PropTypes.string,
uid: PropTypes.string
})
})),
projects: PropTypes.arrayOf(PropTypes.shape({
data: PropTypes.shape({
project_name: PrismicTypes.PrismicTextTypes,
learn_more_link: PropTypes.shape({
link_type: PropTypes.string,
target: PropTypes.string,
url: PropTypes.string
})
})
})),
footerData: PropTypes.arrayOf(PropTypes.shape({
label: PrismicTypes.PrismicTextTypes,
link: PropTypes.shape({
link_type: PropTypes.string,
target: PropTypes.string,
url: PropTypes.string
})
}))
}
export default Layout
ScrollToTop.js:
import React from 'react'
class ScrollToTop extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
window.scrollTo(0, 0)
}
}
componentDidMount() {
window.scrollTo(0, 0)
}
render() {
return this.props.children
}
}
export default (ScrollToTop)
router.js:
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import ScrollToTop from './app/Utils/ScrollToTop'
import routes from './routes'
export default (({prismicCtx, PRISMIC_UNIVERSAL_DATA}) => {
return (
<ScrollToTop>
<Switch>
{routes(prismicCtx, PRISMIC_UNIVERSAL_DATA).map((route, index) => {
const copyRoute = Object.assign({}, route)
if (copyRoute.render) delete copyRoute.component
return <Route key={`route-${index}`} {...copyRoute} />
})}
</Switch>
</ScrollToTop>
)
})
Looking at the different react lifecycle steps, I added the same scrollTo behavior for componentWillUpdate (in ScrollToTop.js), and these pages load at the top from this menu again!
componentWillUpdate() {
window.scrollTo(0, 0)
}
I've playing around with animation implemented with reactjs.
In the app I created a car which drives around a track. On this track there are obstacles, which the car should recognize.
I'm using window.setInterval for the repeating events. Maybe this is not the best option, but actually I don't know how to do else.
Since some changes, there are multiple intervals running.
But actually I don't know the reason for it. Can anybody give me a hint, why the racer component is instantly running in componentdidmount event?
The Racer component is giving the current position and degree / ankle to the Track component. The Track component is storing these values in states and giving it to the Racer component as props. But this should not lead to instantly firing componentdidmount event of Racer component, or?
Here is my code:
App.js
import React, { Component } from 'react';
import Track from './components/track.js';
const uuidv1 = require('uuid/v1');
class App extends Component {
constructor(props) {
super(props);
this.state = {
obstacles: [
{
key: uuidv1(),
position: {
left: 500,
top:10,
},
width: 25,
height: 25,
},
{
key: uuidv1(),
position: {
left: 650,
top:60,
},
width: 25,
height: 25,
}
],
};
}
render() {
return (
<div className="App">
<Track width={800} height={100} obstacles={this.state.obstacles}>
</Track>
</div>
);
}
}
export default App;
Track.js
import React, { Component } from 'react';
import styled from 'styled-components';
import Racer from './racer.js';
import Obstacle from './obstacle';
import centralStrip from '../images/centralStrip.png';
const uuidv1 = require('uuid/v1');
class Track extends Component {
constructor(props) {
super(props);
this.state = {
racerCurrentPosition: {
top: 60,
left:150
},
racerDegree: 0,
};
}
componentDidMount() {
}
handleObstacleCheck(position, racerPosition) {
let obstacleFound = false;
obstacleFound = this.props.obstacles.map((obstacle) => {
let returnValue = false;
let obstacleRect = document.getElementById(obstacle.key).getBoundingClientRect();
if( position.right >= obstacleRect.left && position.right <= obstacleRect.right && racerPosition.top >= obstacleRect.top && racerPosition.bottom <= obstacleRect.bottom) {
returnValue = true;
}
return returnValue;
});
let isObstacleFound = false;
if(obstacleFound.indexOf(true) !== -1) {
isObstacleFound = true;
}
return isObstacleFound;
}
handleRacerPositionChange(position) {
this.setState({
racerCurrentPosition: position,
});
}
handleRacerDegreeChange(newDegree) {
this.setState({
racerDegree: newDegree,
});
}
render() {
return (
<TrackImage key={uuidv1()}
id="track"
width={this.props.width}
height={this.props.height}>
<Racer key={uuidv1()}
position={this.state.racerCurrentPosition}
onRacerPositionChange={this.handleRacerPositionChange.bind(this)}
degree={this.state.racerDegree}
onRacerDegreeChange={this.handleRacerDegreeChange.bind(this)}
obstacleFound={this.state.obstacleFound}
trackWidth={this.props.width}
trackHeight={this.props.height}
onObstacleCheck={this.handleObstacleCheck.bind(this)}
/>
{
this.props.obstacles.map((obstacle) => {
return (
<Obstacle key={obstacle.key}
id={obstacle.key}
position={obstacle.position}
width={obstacle.width}
height={obstacle.height}
/>
);
})
}
</TrackImage>
);
}
}
export default Track;
Racer.js
import React, { Component, Fragment } from 'react';
import styled from 'styled-components';
import HelperDistance from './helpers/distance.js';
import HelperCenterCar from './helpers/centerCar.js';
import racerImage from '../images/racer.png';
const uuidv1 = require('uuid/v1');
class Racer extends Component {
constructor(props) {
super(props);
this.state = {
key: uuidv1(),
intervalId: 0,
speed: 0,
helperForLeftPositioning: 0,
helperForTopPositioning: 0,
isMoving: false,
collision: false,
centerOfCarCoordinates: {
x: 25,
y: 12.5
},
obstacleFound: false,
};
this.start = this.start.bind(this);
this.move = this.move.bind(this);
}
componentDidMount() {
if(this.state.intervalId === 0) {
this.start();
}
}
componentWillUnmount() {
window.clearInterval(this.state.intervalId);
}
start() {
this.setState({
speed: 3,
isMoving: true,
}, () => {
this.createInterval();
});
}
stop() {
this.setState({
speed: 0,
isMoving: false,
}, () => {
window.clearInterval(this.state.intervalId);
});
}
move() {
if(this.state.obstacleFound === true) {
let newDegree;
if(this.props.degree === 0) {
newDegree = 360;
}
newDegree--;
this.props.onRacerDegreeChange(newDegree);
}
this.step();
}
step() {
if(this.state.isMoving) {
//...calculate new position
this.setState({
helperForTopPositioning: helperForTopPositioning,
helperForLeftPositioning: helperForLeftPositioning,
},() => {
let position = {
left: positionNewLeft,
top: positionNewTop
};
this.props.onRacerPositionChange(position);
});
}
}
createInterval = () => {
let intervalId = window.setInterval(() => {
this.move();
console.log("IntervalId: " + intervalId);
},100);
this.setState({
intervalId: intervalId,
})
}
handleDistanceChange(position) {
let racerRect = document.getElementById(this.state.key).getBoundingClientRect();
let obstacleFound = this.props.onObstacleCheck(position, racerRect);
if(this.state.obstacleFound !== obstacleFound) {
this.setState({
obstacleFound: obstacleFound
});
}
}
render() {
return (
<Fragment>
<Car key={this.state.key} id={this.state.key} position={this.props.position} degree={this.props.degree}>
<HelperCenterCar key={uuidv1()} position={this.state.centerOfCarCoordinates} degree={this.props.degree} />
<HelperDistance key={uuidv1()} onChange={this.handleDistanceChange.bind(this)} position={this.state.centerOfCarCoordinates} degree={this.props.degree} />
</Car>
</Fragment>
);
}
}
export default Racer;
The HelperCenterCar and HelperDistance are components, which helps to identify, if there is an obstacle in the way. I'll post just the code of HelperDistance, because here instantly state updates are fired.
HelperDistance.js
import React, { Component } from 'react';
import styled from 'styled-components';
const uuidv1 = require('uuid/v1');
class HelperDistance extends Component {
constructor(props) {
super(props);
this.state = {
key: uuidv1(),
};
}
componentDidMount() {
this.handleOnChange();
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.handleOnChange();
}
handleOnChange() {
let position = document.getElementById(this.state.key).getBoundingClientRect();
this.props.onChange(position);
}
render() {
return (
<Line id={this.state.key} key={this.state.key} position={this.props.position} degree={this.props.degree} />
);
}
}
export default HelperDistance;
I have two components, the parent "App" component and the Hero component which contains an image and left and right arrow.
I want to use the right arrow to move the imageIndex + 1 until it reaches the images.length, and I want to have the left arrow to have a condition that I can't subtract if imageIndex = 0.
So something like: ( This part of the code is not added in my code yet because I keep getting undefined)
if (this.props.imageIndex > 0) {
this.setState({
// decrease the imageIndex by 1
})
}
if (this.props.imageIndex < this.props.images.length - 1){
this.setState({
// increase the imageIndex by 1
})
}
will be the condition or something like it.
App.jS (Parent Component)
export default class App extends Component {
constructor() {
super();
this.state = {
language: "english",
render: 'overview',
imageIndex: 0,
}
}
render() {
// to make sure the state changes
console.log(this.state.language)
const {render} = this.state
return <Fragment>
<Hero imageIndex = {this.state.imageIndex} />
</Fragment>;
}
}
How would I add that in my Hero Component which contains this code:
Hero.js
class Hero extends Component {
constructor(props) {
super(props);
this._ToggleNext = this._ToggleNext.bind(this);
}
_ToggleNext(props) {
console.log(this.props.listing.images.length)
console.log(this.props.imageIndex)
}
_TogglePrev(props) {
console.log(this.props.listing.images.length)
console.log(this.props.imageIndex)
}
render() {
const { listing: { images = [], name, location = {} } = {} } = this.props;
return <div className="hero">
<img src={images[0]} alt="listing" />
<a onClick={this._TogglePrev}) className="hero__arrow hero__arrow--left">◀</a>
<a onClick={this._ToggleNext} className="hero__arrow hero__arrow--right">▶</a>
<div className="hero__info">
<p>{location.city}, {location.state}, {location.country}</p>
<h1>{name}</h1>
</div>
</div>;
}
}
const getHero = gql`
query getHero {
listing {
name
images
location {
address,
city,
state,
country
}
}
}
`;
export default function HeroHOC(props) {
return <Query
query={getHero}
>
{({ data }) => (
<Hero
{...props}
listing={data && data.listing || {}} // eslint-disable-line no-mixed-operators
/>
)}
</Query>;
}
One solution is to define the data and functionality in the parent component, in this case App, and pass those down as props to the child which will focus on the rendering.
(code not tested but should give you the basic idea)
class App extends Component {
state = {
imageIndex: 0,
listing: {
images: ['foo.jpg', 'bar.jpg'],
name: 'foo',
location: {...}
}
}
_ToggleNext = () => {
const { imageIndex, listing } = this.state;
if (imageIndex === listing.images.length - 1) {
this.setState({imageIndex: 0});
}
}
_TogglePrev = () => {
const { imageIndex, listing } = this.state;
if (imageIndex === 0) {
this.setState({imageIndex: listing.images.length - 1});
}
}
render() {
return (
<Fragment>
<Hero
listing={this.state.listing}
imageIndex={this.state.imageIndex}
toggleNext={this._ToggleNext}
togglePrev={this._TogglePrev}
/>
</Fragment>
);
}
}
Hero component:
const Hero = props => {
const { listing, imageIndex, togglePrev, toggleNext } = props;
return (
<div className="hero">
<img src={listing.images[imageIndex]}/>
<a onClick={togglePrev})>◀</a>
<a onClick={toggleNext}>▶</a>
<div className="hero__info">
...
</div>
</div>
);
};
I have created two separate components and a parent component. I am trying to see how I can connect them so that I can have the dropdown for the children vanish when their checkbox is unchecked. I think I may have created this so the 2 components can't communicate, but I wanted to see if there was a way to get them to. Been trying different ways, but cannot seem to figure it out.
This is the parent component. It builds sections from some data and renders a checkbox treeview with the first (parent) checkbox having a dropdown. When the third option is selected in this dropdown, it renders in a dropdown for each child checkbox. I am trying to see if I can have the child dropdowns vanish when the checkbox is unchecked, but I can't seem to get the 2 components to communicate.
export default class CheckboxGroup extends PureComponent {
static propTypes = {
data: PropTypes.any.isRequired,
onChange: PropTypes.func.isRequired,
counter: PropTypes.number,
};
mapParents = (counter, child) => (
<li key={child.get('name')} className='field'>
<SegmentHeader style={segmentStyle} title={child.get('label')} icon={child.get('icon')}>
<div className='fields' style={zeroMargin}>
<div className='four wide field'>
<TreeCheckbox
label={`Grant ${child.get('label')} Permissions`}
counter={counter}
onChange={this.props.onChange}
/>
{child.get('items') && this.buildTree(child.get('items'), counter + child.get('name'))}
</div>
<div className='twelve wide field'>
<GrantDropdown label={child.get('label')} childItems={child.get('items')}/>
</div>
</div>
</SegmentHeader>
</li>
)
mapDataArr = (counter) => (child) => (
(counter === 0 || counter === 1000) ?
this.mapParents(counter, child)
:
<li key={child.get('name')}>
<TreeCheckbox label={child.get('label')} onChange={this.props.onChange}/>
{child.get('items') && this.buildTree(child.get('items'), counter + child.get('name'))}
</li>
)
buildTree = (dataArr, counter) => (
<ul key={counter} style={listStyle}>
{dataArr.map(this.mapDataArr(counter))}
</ul>
)
render() {
return (
<div className='tree-view'>
{this.buildTree(this.props.data, this.props.counter)}
</div>
);
}
}
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const pointer = { cursor: 'pointer' };
class TreeCheckbox extends PureComponent {
static propTypes = {
onChange: PropTypes.func,
label: PropTypes.string,
currentPerson: PropTypes.any,
};
componentDidMount() {
if (this.props.currentPerson.get('permissions').includes(this.props.label)) {
this.checkInput.checked = true;
this.changeInput(this.checkInput);
}
}
getLiParents = (el, parentSelector) => {
if (!parentSelector) parentSelector = document; // eslint-disable-line
const parents = [];
let parent = el.parentNode;
let o;
while (parent !== parentSelector) {
o = parent;
if (parent.tagName === 'LI') parents.push(o);
parent = o.parentNode;
}
return parents;
}
traverseDOMUpwards = (startingEl, steps) => {
let elem = startingEl;
for (let i = 0; i < steps; i++) {
elem = elem.parentNode;
}
return elem;
}
markIt = (nodeElem, checkIt, indeter) => {
const node = nodeElem;
const up = this.traverseDOMUpwards(node, 1);
node.checked = checkIt;
node.indeterminate = indeter;
this.props.onChange(up.children[1].innerText, checkIt);
}
changeInput = (event) => {
const e = event === this.checkInput ? event : event.target;
const selector = 'input[type="checkbox"]';
const querySelector = (el) => el.querySelectorAll(selector);
const container = this.traverseDOMUpwards(e, 2);
const markAllChildren = querySelector(container.parentNode);
const checked = e.tagName === 'LABEL' ? !markAllChildren[0].checked : e.checked;
const siblingsCheck = (element) => {
let onesNotRight = false;
const sibling = [].slice.call(element.parentNode.children);
sibling.filter(child => child !== element).forEach(elem => {
if (querySelector(elem)[0].checked !== querySelector(element)[0].checked) {
onesNotRight = true;
}
});
return !onesNotRight;
};
const checkRelatives = (ele) => {
let el = ele;
if (el.tagName === 'DIV') el = el.parentNode;
if (el.tagName !== 'LI') return;
const parentContainer = this.traverseDOMUpwards(el, 2);
if (siblingsCheck(el) && checked) {
this.markIt(querySelector(parentContainer)[0], true, false);
checkRelatives(parentContainer);
} else if (siblingsCheck(el) && !checked) {
const parent = this.traverseDOMUpwards(el, 2);
const indeter = parent.querySelectorAll(`${selector}:checked`).length > 0;
this.markIt(querySelector(parent)[0], false, indeter);
checkRelatives(parent);
} else {
for (const child of this.getLiParents(el)) {
this.markIt(querySelector(child)[0], false, true);
}
}
};
for (const children of markAllChildren) {
this.markIt(children, checked, false);
}
checkRelatives(container);
};
getRef = (input) => { this.checkInput = input; }
render() {
const { label } = this.props;
return (
<div className='permission-item'>
<div className='ui checkbox'>
<input type='checkbox' onChange={this.changeInput} ref={this.getRef}/>
<label onClick={this.changeInput} style={pointer}>
{label}
</label>
</div>
</div>
);
}
}
const mapStatetoProps = (state) => ({
currentPerson: state.get('currentPerson'),
});
export default connect(mapStatetoProps)(TreeCheckbox);
class GrantDropdown extends AbstractSettingsComponent {
static propTypes = {
label: PropTypes.string,
currentPerson: PropTypes.any,
counter: PropTypes.number,
permissionOptions: PropTypes.any,
};
state = {
items: new List(),
}
componentDidMount() {
if (this.props.childItems) {
this.getAllChildLabels(this.props.childItems);
}
}
getAllChildLabels = (childItems) => {
let list = new List();
for (const item of childItems) {
list = list.push(item.get('label'));
if (item.get('items')) {
for (const childItem of item.get('items')) {
list = list.push(childItem.get('label'));
}
}
}
this.setState({ items: list });
}
handlePermissionChange = (label) => (e, { value }) => {
this.updatePerson(['locationsPermissionsMap', label], value);
}
mapItems = (val, i) => { // eslint-disable-line
const locationVal = this.props.currentPerson.getIn(['locationsPermissionsMap', val]);
return (
<div className={locationVal === 2 ? 'two fields' : 'field'} style={zeroMarginBottom} key={i}>
<OptionSelector
options={this.firstThreePermissionOpt()}
defaultValue={locationVal || 0}
onChange={this.handlePermissionChange(val)}
/>
{locationVal === 2 &&
<div className='field' style={zeroMarginBottom}>
<LocationMultiSelect name={val} {...this.props}/>
</div>
}
</div>
);
}
render() {
const { label, currentPerson } = this.props;
if (!currentPerson.get('permissions').includes(label)) {
return null;
}
const locationLabel = currentPerson.getIn(['locationsPermissionsMap', label]);
return (
<div className={ locationLabel === 2 ? 'two fields' : 'field'} style={zeroMarginBottom}>
<div className='field'>
<OptionSelector
options={this.getPermissionOptions()}
defaultValue={currentPerson.getIn(['locationsPermissionsMap', label]) || 0}
onChange={this.handlePermissionChange(label)}
/>
{locationLabel === 3 && this.state.items.map(this.mapItems)}
</div>
{locationLabel === 2 &&
<div className='field'>
<LocationMultiSelect name={label} {...this.props}/>
</div>
}
</div>
);
}
}
const mapStatetoProps = (state) => ({
currentPerson: state.get('currentPerson'),
locations: state.get('locations'),
});
export default connect(mapStatetoProps)(GrantDropdown);
What you can do is set couple of props to send to child component to re-render them.
Example
export default class CheckBoxComponent extends React.Component {
changeInput() {
this.props.onCheckedChanged();
}
render() {
return(
<div className='permission-item'>
<div className='ui checkbox'>
<input type='checkbox' onChange={this.changeInput} ref={this.getRef}/>
<label onClick={this.changeInput.bind(this)} style={pointer}>
{label}
</label>
</div>
</div>
)
}
}
export default class DropDownComponent extends React.Component {
renderSelect() {
// here render your select and options
}
render() {
return(
<div>
{this.props.checkboxChecked === false ? this.renderSelect : null}
</div>
)
}
}
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
checkboxChecked: false
};
}
onCheckedChanged() {
this.setState({ checkboxChecked: !this.state.checkboxChecked });
}
render() {
return(
<div>
<CheckBoxComponent onCheckedChanged={this.onCheckedChanged.bind(this)} />
<DropDownComponent checkboxChecked={this.state.checkboxChecked} />
</div>
)
}
}
You can set the <input/> name attribute into a corresponding property in your state so the handler can get the name of the list / input via the event parameter and set the state respectively.
Then you can conditionally render the Dropdown according to the state.
Here is a small example of such behavior:
const lists = [
[
{ value: "0", text: "im 0" },
{ value: "1", text: "im 1" },
{ value: "2", text: "im 2" }
],
[
{ value: "a", text: "im a" },
{ value: "b", text: "im b" },
{ value: "c", text: "im c" }
]
];
const DropDown = ({ options }) => {
return (
<select>
{options.map(opt => <option value={opt.value}>{opt.text}</option>)}
</select>
);
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
showList0: false,
showList1: true
};
this.toggleCheck = this.toggleCheck.bind(this);
}
toggleCheck(e) {
const listName = e.target.name;
this.setState({ [listName]: !this.state[listName] });
}
render() {
return (
<div>
{lists.map((o, i) => {
const listName = `showList${i}`;
const shouldShow = this.state[listName];
return (
<div>
<input
type="checkbox"
name={listName}
checked={shouldShow}
onChange={this.toggleCheck}
/>
{shouldShow && <DropDown options={o} />}
</div>
);
})}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>