I'm trying to modify a Spinner component by adding a new method Spinner.moveToPosition so that it will spin to a specific symbol, as opposed to Spinner.forceUpdateHandler which spins it to a random symbol.
Here's my CodePen: https://codepen.io/gameveloster/pen/qBxJPVK
Original CodePen: https://codepen.io/antibland/pen/ypagZd
However, when I added a "SPIN!" button where clicking the button will get them to spin to positions 0, 0 and 1 so that the left two spinners will display the same image
const handleClick = () => {
// The first and second symbols should be the same after spinning, but they are not
_child1?.current?.moveToPosition(0);
_child2?.current?.moveToPosition(0);
_child3?.current?.moveToPosition(1);
};
they end up not doing that:
What might be the issue here? Thank you!
Spinner.js
import React from "react";
class Spinner extends React.Component {
constructor(props) {
super(props);
this.forceUpdateHandler = this.forceUpdateHandler.bind(this);
}
moveToPosition(position) {
if (this.timer) {
clearInterval(this.timer);
}
this.setState({
position: position * Spinner.iconHeight,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
forceUpdateHandler() {
this.reset();
}
reset() {
if (this.timer) {
clearInterval(this.timer);
}
this.start = this.setStartPosition();
this.setState({
position: this.start,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
state = {
position: 0,
lastPosition: null,
};
static iconHeight = 188;
multiplier = Math.floor(Math.random() * (4 - 1) + 1);
start = this.setStartPosition();
speed = Spinner.iconHeight * this.multiplier;
setStartPosition() {
return Math.floor(Math.random() * 9) * Spinner.iconHeight * -1;
}
moveBackground() {
this.setState({
position: this.state.position - this.speed,
timeRemaining: this.state.timeRemaining - 100,
});
}
getSymbolFromPosition() {
let { position } = this.state;
const totalSymbols = 9;
const maxPosition = Spinner.iconHeight * (totalSymbols - 1) * -1;
let moved = (this.props.timer / 100) * this.multiplier;
let startPosition = this.start;
let currentPosition = startPosition;
for (let i = 0; i < moved; i++) {
currentPosition -= Spinner.iconHeight;
if (currentPosition < maxPosition) {
currentPosition = 0;
}
}
this.props.onFinish(currentPosition);
}
tick() {
if (this.state.timeRemaining <= 0) {
clearInterval(this.timer);
this.getSymbolFromPosition();
} else {
this.moveBackground();
}
}
componentDidMount() {
clearInterval(this.timer);
this.setState({
position: this.start,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
render() {
let { position, current } = this.state;
return (
<div
style={{ backgroundPosition: "0px " + position + "px" }}
className={`icons`}
/>
);
}
}
export default Spinner;
App.tsx
import React, { useRef } from "react";
import "./App.css";
import Spinner from "./Spinner.js";
const App = () => {
const [matches, setMatches] = React.useState<number[]>([]);
const _child1 = useRef<Spinner | null>(null);
const _child2 = useRef<Spinner | null>(null);
const _child3 = useRef<Spinner | null>(null);
const handleClick = () => {
// The first and second symbols should be the same after spinning, but they are not
_child1?.current?.moveToPosition(0);
_child2?.current?.moveToPosition(0);
_child3?.current?.moveToPosition(1);
};
const finishHandler = (value: number) => {
setMatches([...matches, value]);
if (matches.length === 3) {
console.log("Done");
emptyArray();
}
};
const emptyArray = () => {
setMatches([]);
};
return (
<div>
<div className={`spinner-container`}>
<Spinner onFinish={finishHandler} ref={_child1} timer="1000" />
<Spinner onFinish={finishHandler} ref={_child2} timer="1400" />
<Spinner onFinish={finishHandler} ref={_child3} timer="2200" />
<div className="gradient-fade"></div>
</div>
<button onClick={handleClick}>SPIN!!!</button>
</div>
);
};
export default App;
Related
I'm trying to rewrite the App component in this CodePen as a Functional component using Typescript.
However, I am getting error like this when trying to run it:
ERROR in src/App.tsx:13:14
TS2339: Property 'forceUpdateHandler' does not exist on type 'MutableRefObject<Spinner | null>'.
11 |
12 | const handleClick = () => {
> 13 | _child1?.forceUpdateHandler();
| ^^^^^^^^^^^^^^^^^^
14 | _child2?.forceUpdateHandler();
15 | _child3?.forceUpdateHandler();
16 | };
What is the correct way to handle Spinner.forceUpdateHandler?
Here's my attempt:
App.tsx
Rewriting the class component as a functional component
This has been simplified from the original to focus on the problematic area
import React, { useRef } from "react";
import "./App.css";
import Spinner from "./Spinner.js";
const App = () => {
const [matches, setMatches] = React.useState<number[]>([]);
const _child1 = useRef<Spinner | null>(null);
const _child2 = useRef<Spinner | null>(null);
const _child3 = useRef<Spinner | null>(null);
const handleClick = () => {
_child1?.forceUpdateHandler();
_child2?.forceUpdateHandler();
_child3?.forceUpdateHandler();
};
const finishHandler = (value: number) => {
setMatches([...matches, value]);
if (matches.length === 3) {
console.log("Done");
emptyArray();
}
};
const emptyArray = () => {
setMatches([]);
};
return (
<div>
<div className={`spinner-container`}>
<Spinner onFinish={finishHandler} ref={_child1} timer="1000" />
<Spinner onFinish={finishHandler} ref={_child2} timer="1400" />
<Spinner onFinish={finishHandler} ref={_child3} timer="2200" />
<div className="gradient-fade"></div>
</div>
<button onClick={handleClick}>SPIN!!!</button>
</div>
);
};
export default App;
Spinner.js
Same as in the above CodePen, with imports and exports added
import React from "react";
class Spinner extends React.Component {
constructor(props) {
super(props);
this.forceUpdateHandler = this.forceUpdateHandler.bind(this);
}
forceUpdateHandler() {
this.reset();
}
reset() {
if (this.timer) {
clearInterval(this.timer);
}
this.start = this.setStartPosition();
this.setState({
position: this.start,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
state = {
position: 0,
lastPosition: null,
};
static iconHeight = 188;
multiplier = Math.floor(Math.random() * (4 - 1) + 1);
start = this.setStartPosition();
speed = Spinner.iconHeight * this.multiplier;
setStartPosition() {
return Math.floor(Math.random() * 9) * Spinner.iconHeight * -1;
}
moveBackground() {
this.setState({
position: this.state.position - this.speed,
timeRemaining: this.state.timeRemaining - 100,
});
}
getSymbolFromPosition() {
let { position } = this.state;
const totalSymbols = 9;
const maxPosition = Spinner.iconHeight * (totalSymbols - 1) * -1;
let moved = (this.props.timer / 100) * this.multiplier;
let startPosition = this.start;
let currentPosition = startPosition;
for (let i = 0; i < moved; i++) {
currentPosition -= Spinner.iconHeight;
if (currentPosition < maxPosition) {
currentPosition = 0;
}
}
this.props.onFinish(currentPosition);
}
tick() {
if (this.state.timeRemaining <= 0) {
clearInterval(this.timer);
this.getSymbolFromPosition();
} else {
this.moveBackground();
}
}
componentDidMount() {
clearInterval(this.timer);
this.setState({
position: this.start,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
render() {
let { position, current } = this.state;
return (
<div
style={{ backgroundPosition: "0px " + position + "px" }}
className={`icons`}
/>
);
}
}
export default Spinner;
Ref's hold the actual reference in the current property, so it should actually be:
const handleClick = () => {
_child1?.current?.forceUpdateHandler();
_child2?.current?.forceUpdateHandler();
_child3?.current?.forceUpdateHandler();
};
You can read more about it here
just an idea may be you should try redefining the Spinner type in your functional component
type SpinnerProps ={
forceUpdateHandler: () => void,
startPosition: () => number,
.... // TODO all the other props need to be defined
}
const _child1 = useRef<SpinnerProps | null>(null);
We recently worked on an auto-scrolling while freely swipeable component using React.js. The implementation idea is inspired by this article
And we've made something like this in React:
import React, { Component } from "react";
import PropTypes from "prop-types";
import "./AutoScroller.css";
const NUM_OF_CLONES = 10;
const AUTO_SCROLL_OFFSET = 1; // min offset of scrollTo is 1
const AUTO_SCROLL_INTERVAL = 32; // 1000 ms / 30 fps
export default class AutoScroller extends Component {
static propTypes = {
contents: PropTypes.array.isRequired,
itemWidth: PropTypes.number.isRequired,
numsOfItemsPerScreen: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.autoScrollerRef = React.createRef();
this.currentPosition = 0;
this.autoScrollTimer = null;
this.scrollingTimer = null;
/* boolean status */
this.isTouch = false;
this.isScrolling = false;
}
componentDidMount() {
this.startAutoScroll();
this.autoScrollerRef.current.addEventListener(
"touchstart",
this.touchStartHandler
);
this.autoScrollerRef.current.addEventListener(
"touchend",
this.touchEndHandler
);
this.autoScrollerRef.current.addEventListener("scroll", this.scrollHandler);
this.autoScrollerRef.current.addEventListener(
"contextmenu",
this.contextMenuHandler
);
}
componentWillUnmount() {
this.clearAutoScroll();
this.clearScrollingTimer();
this.autoScrollerRef.current.removeEventListener(
"touchstart",
this.touchStartHandler
);
this.autoScrollerRef.current.removeEventListener(
"touchend",
this.touchEndHandler
);
this.autoScrollerRef.current.removeEventListener(
"scroll",
this.scrollHandler
);
this.autoScrollerRef.current.removeEventListener(
"contextmenu",
this.contextMenuHandler
);
}
touchStartHandler = () => {
this.isTouch = true;
this.clearAutoScroll();
};
touchEndHandler = () => {
this.isTouch = false;
if (!this.isScrolling) {
this.currentPosition = this.autoScrollerRef.current.scrollLeft;
this.startAutoScroll();
}
};
scrollHandler = () => {
const {
contents: { length },
itemWidth
} = this.props;
this.isScrolling = true;
this.currentPosition = this.autoScrollerRef.current.scrollLeft;
const maxOffset = length * itemWidth;
if (this.currentPosition > maxOffset) {
const offset = this.currentPosition - maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
} else if (this.currentPosition <= 0) {
const offset = this.currentPosition + maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
}
/***
* note: there will be only one timer, and the timer is only created by the very last scroll
* only when the scroll event is not triggered anymore, the timer starts to get executed.
*/
if (this.scrollingTimer) {
clearTimeout(this.scrollingTimer);
}
this.scrollingTimer = setTimeout(() => {
this.isScrolling = false;
/***
* note: resume auto-scroll when the momentum scroll (after finger leaves) stalls the scroll
*/
if (!this.isTouch) {
this.startAutoScroll();
}
}, 300);
};
contextMenuHandler = (event) => {
event.preventDefault();
};
startAutoScroll = () => {
if (!this.autoScrollTimer) {
this.autoScrollTimer = setInterval(this.autoScroll, AUTO_SCROLL_INTERVAL);
}
};
clearAutoScroll = () => {
if (this.autoScrollTimer) {
clearInterval(this.autoScrollTimer);
this.autoScrollTimer = null;
}
};
clearScrollingTimer = () => {
if (this.scrollingTimer) {
clearTimeout(this.scrollingTimer);
this.scrollingTimer = null;
}
};
autoScroll = () => {
const {
contents: { length },
itemWidth,
numsOfItemsPerScreen
} = this.props;
if (this.currentPosition < 0) {
this.currentPosition = 0;
}
if (length > numsOfItemsPerScreen) {
const position = this.currentPosition + AUTO_SCROLL_OFFSET;
this.autoScrollerRef.current.scrollTo(position, 0);
const maxOffset = length * itemWidth;
if (this.currentPosition > maxOffset) {
const offset = this.currentPosition - maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
} else {
this.currentPosition = position;
}
}
};
getWrappedData = () => {
const { contents } = this.props;
const { length } = contents;
const numberOfClones = length < NUM_OF_CLONES ? length : NUM_OF_CLONES;
return [...contents, ...contents.slice(0, numberOfClones)];
};
render() {
const { itemGap, lineHeight } = this.props;
return (
<div className="auto-scroller" ref={this.autoScrollerRef}>
<ul>
{this.getWrappedData().map((content, index) => (
<Item
key={`auto-scroller-item-${index}`}
content={content}
itemGap={itemGap}
lineHeight={lineHeight}
/>
))}
</ul>
</div>
);
}
}
class Item extends Component {
static propTypes = {
content: PropTypes.object.isRequired,
itemGap: PropTypes.number,
lineHeight: PropTypes.number
};
render() {
const { content, itemGap = 10 } = this.props;
return (
<li
className="auto-scroller__item"
style={{ paddingRight: `${itemGap}px` }}
>
<div className="auto-scroller__item__content">
<img draggable={false} src={content.imgUrl} />
<div className="auto-scroller__item__content__title">
{content.title}
</div>
</div>
</li>
);
}
}
You can test with the demo from PlayCode (source code).
Just open the link with Safari on the iPhone.
What I observed was every time when it was on the boundary cases, the image started to flicker.
Further, if you swipe it with your finger forth and back on that point, the whole UI started to flicker. (see this screen recording) However, we didn't spot this glitch on Android devices.
Any possible solutions are welcome. Does anyone encounter something like this before?
removing overflow-y: hidden; and overflow-x: auto; from autoscroller.css
solved it on my end.
another solution would be to add z-index: 1; and scroll-behavior: smooth; to .auto-scroller
let me know if it worked!
please help me! I'm using context API to pass the tracks to MucsicPlayer. but whenever I set setTracks state audio element src updates but audioRef.current doesn't update. I inspected it and saw that audioRef.current = <audio preload="auto" src(unknown)>. so ref src does not update. what should I do.
import React, { useState, useEffect, useRef, useContext } from 'react'
import { TrackContext } from '../../music/TrackContext'
const MusicPlayer = () => {
const [tracks, setTracks] = useContext(TrackContext)
console.log(tracks)
// states
const [trackIndex, setTrackIndex] = useState(0)
console.log(trackIndex)
const [trackProgress, setTrackProgress] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
// eslint-disable-next-line
const [volume, setVolume] = useState(1)
const { title, artist, image, audioSrc } = tracks[trackIndex]
//refs
const audio = new Audio(audioSrc)
const audioRef = useRef(audio)
const intervalRef = useRef()
const isReady = useRef(false)
console.log(audioRef.current)
const { duration } = audioRef.current
const toPrevTrack = () => {
if (trackIndex - 1 < 0) {
setTrackIndex(tracks.length - 1)
} else {
setTrackIndex(trackIndex - 1)
}
}
const toNextTrack = () => {
if (trackIndex < tracks.length - 1) {
setTrackIndex(trackIndex + 1)
} else {
setTrackIndex(0)
}
}
const startTimer = () => {
clearInterval(intervalRef.current)
intervalRef.current = setInterval(() => {
if (audioRef.current.ended) {
toNextTrack()
} else {
setTrackProgress(audioRef.current.currentTime);
}
}, [1000])
}
useEffect(() => {
if (isPlaying) {
audioRef.current.play()
startTimer();
} else {
clearInterval(intervalRef.current)
audioRef.current.pause()
}
// eslint-disable-next-line
}, [isPlaying])
useEffect(() => {
return () => {
audioRef.current.pause()
clearInterval(intervalRef.current)
}
}, [])
useEffect(() => {
audioRef.current.play()
audioRef.current = new Audio(audioSrc)
setTrackProgress(audioRef.current.currentTime)
if (isReady.current) {
audioRef.current.play()
setIsPlaying(true)
startTimer()
} else {
isReady.current = true
}
// eslint-disable-next-line
}, [trackIndex])
const onScrub = (value) => {
clearInterval(intervalRef.current)
audioRef.current.currentTime = value
setTrackProgress(audioRef.current.currentTime)
}
const onScrubEnd = () => {
if (!isPlaying) {
setIsPlaying(true);
}
startTimer();
}
const onScrubVolume = (value) => {
audioRef.current.volume = value
setVolume(audioRef.current.value)
}
function formatMinutes(sec) {
return new Date(sec * 1000).toUTCString().split(" ")[4].substr(3, 8)
}
const currentPercentage = duration ? `${(trackProgress / duration) * 100}%` : '0%';
const trackStyling = `-webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, #fff), color-stop(${currentPercentage}, #777))`;
return (
<div className="player">
<div className="left-block">
<div className="art">
<img src={image} alt="" />
</div>
<div className="song-details">
<div className="song-name">{title}</div>
<div className="artist-name">
{artist}
</div>
</div>
</div>
<div className="center-block">
<div className="song-progress">
<div>{formatMinutes(audioRef.current.currentTime)}</div>
<input
value={trackProgress}
step="1"
min="1"
max={duration ? duration : `${duration}`}
onChange={(e) => onScrub(e.target.value)}
onMouseUp={onScrubEnd}
onKeyUp={onScrubEnd}
style={{ background: trackStyling }}
type="range" />
<div>{duration ? formatMinutes(duration) : "00:00"}</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default MusicPlayer
useRef will not get re-initialized on every render.
So it will stay the same as it was initialized the very first time.
So whenever you are switching your track you have to update the audioRef too.
Change your toPrevTrack and toNextTrack
const toPrevTrack = () => {
const prevIndex = trackIndex - 1 < 0 ? tracks.length - 1 : trackIndex - 1;
const { audioSrc } = tracks[prevIndex]
audioRef.current = new Audio(audioSrc);
}
const toNextTrack = () => {
const nextIndex = trackIndex < tracks.length - 1 ? trackIndex + 1 : 0;
const { audioSrc } = tracks[nextIndex]
audioRef.current = new Audio(audioSrc);
}
I have a parent component which has timer component inside it. Timer starts at 15 minutes and count downs till 0. When my timer shows time as 0 I want to trigger a submit button event, submit button is inside Quiz Component (Quiz Component is also a child component of Parent Component). I found probably I can use MutationObserver when p tag changes. I am not sure whether it's the correct and only approach or there is better way to achieve this.
Parent Component:
import React, { Component } from 'react';
import '../css/App.css'
import Quiz from './Quiz';
import Timer from './Timer';
import { connect } from 'react-redux';
import { ActionTypes } from '../redux/constants/actionTypes';
import { saveQuizAll, getQuizIndex } from '../commonjs/common.js';
const mapStateToProps = state => { return { ...state.quiz, ...state.quizAll } };
const mapDispatchToProps = dispatch => ({
onQuizLoad: payload => dispatch({ type: ActionTypes.QuizLoad, payload }),
onQuizChange: payload => dispatch({ type: ActionTypes.QuizAnswerAll, payload }),
onPagerUpdate: payload => dispatch({ type: ActionTypes.PagerUpdate, payload })
});
class QuizContainer extends Component {
state = {
quizes: [
{ id: 'data/class1.json', name: 'Class 1' },
{ id: 'data/class2.json', name: 'Class 2' },
{ id: 'data/class3.json', name: 'Class 3' },
{ id: 'data/class4.json', name: 'Class 4' },
],
quizId: 'data/class1.json'
};
pager = {
index: 0,
size: 1,
count: 1
}
componentDidMount() {
console.log('componentDidMount');
this.load(this.state.quizId);
}
load(quizId, isValReload) {
console.log('In load');
let url = quizId || this.props.quizId;
if (isValReload) {
let quiz = this.props.quizAll.find(a => url.indexOf(`${a.id}.`) !== -1);
console.log('In load quiz : ', quiz);
this.pager.count = quiz.questions.length / this.pager.size;
this.props.onQuizLoad(quiz);
this.props.onPagerUpdate(this.pager);
}
else {
fetch(`../${url}`).then(res => res.json()).then(res => {
let quiz = res;
quiz.questions.forEach(q => {
q.options.forEach(o => o.selected = false);
});
quiz.config = Object.assign(this.props.quiz.config || {}, quiz.config);
this.pager.count = quiz.questions.length / this.pager.size;
this.props.onQuizLoad(quiz);
this.props.onPagerUpdate(this.pager);
});
}
}
//This event implements restriction to change class without finishing curretnly selectd class
onClassClick = (e) => {
let qus = this.props.quiz.questions;
// console.log(qus);
let isNotAllAns = qus.some((q, i) => {
var isNot = false;
if (q.answerType.id !== 3 && q.answerType.id !== 4) {
isNot = (q.options.find((o) => o.selected === true)) === undefined;
}
else {
// console.log('q', q);
isNot = ((q.answers === "" || q.answers.length === 0));
}
return isNot;
});
if (isNotAllAns) {
alert('Please complete the quiz.');
e.stopPropagation();
}
}
/*
saveQuizAll(_quizAll, _quiz) {
let allQuiz = [];
// , _quizAll, _quiz;
// if (true) {
// _quiz = this.quiz;
// _quizAll = this.quizAll;
// }
console.log(this, _quiz, _quizAll);
if (_quiz.questions.length !== 0) {
if (_quizAll.length !== undefined) {
console.log('Not Initial Setup Splice', _quiz.id);
allQuiz = _quizAll;
const qIndex = this.getQuizIndex(_quiz.id.toString());
if (qIndex > -1) {
allQuiz.splice(qIndex, 1, _quiz);
}
else {
allQuiz.splice(_quizAll.length, 0, _quiz);
// allQuiz.splice(this.props.quizAll.length-1, 0, this.props.quizAll, this.props.quiz);
}
}
else {
allQuiz[0] = _quiz;
}
return allQuiz;
// if (true) {
// this.onQuizChange(allQuiz);
// }
}
}
*/
onChange = (e) => {
// console.log(this.props.quizAll, this.props.quizAll.length);
let allQuiz = [];
allQuiz = saveQuizAll(this.props.quizAll, this.props.quiz);
//below code converted into saveQuizAll funstion
/*
if (this.props.quizAll.length !== undefined) {
console.log('Not Initial Setup Splice', this.props.quiz.id);
allQuiz = this.props.quizAll;
const qIndex = this.getQuizIndex(this.props.quiz.id.toString());
if (qIndex > -1) {
allQuiz.splice(qIndex, 1, this.props.quiz);
}
else {
allQuiz.splice(this.props.quizAll.length, 0, this.props.quiz);
// allQuiz.splice(this.props.quizAll.length-1, 0, this.props.quizAll, this.props.quiz);
}
}
else {
allQuiz[0] = this.props.quiz;
}
*/
// console.log('allQuiz Out - ', allQuiz);
this.props.onQuizChange(allQuiz);
console.log('Check QuizAll - ', this.props.quizAll);
const aQuiz = JSON.parse(JSON.stringify(this.props.quizAll));
this.setState({ quizId: e.target.value });
if (aQuiz.length !== undefined && getQuizIndex(this.props.quizAll, e.target.value) > -1) {
// console.log(aQuiz.findIndex(a => e.target.value.indexOf(`${a.id}.`) !== -1));
this.load(e.target.value, true);
}
else {
this.setState({ quizId: e.target.value });
this.load(e.target.value, false);
}
}
// getQuizIndex(qID) {
// return this.props.quizAll.findIndex(a => (qID.indexOf(`${a.id}.`) !== -1 || qID.indexOf(`${a.id}`) !== -1));
// }
render() {
return (
<div className="container">
<header className="p-2">
<div className="row">
<div className="col-6">
<h3>DADt Application</h3>
</div>
<div className="col-6 text-right">
<label className="mr-1">Select Quiz:</label>
<select onChange={this.onChange} onClick={this.onClassClick}>
{this.state.quizes.map(q => <option key={q.id} value={q.id}>{q.name}</option>)}
</select>
</div>
</div>
</header>
<Timer duration={900}/>
<Quiz quiz={this.state.quiz} quizId={this.state.quizId} saveAll={saveQuizAll} mode={this.state.mode} />
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(QuizContainer);
Here is my Timer Component
import React, { Component } from 'react'
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
seconds: 0
};
}
tick() {
this.setState((prevState) => ({
seconds: prevState.seconds + 1
}));
}
componentDidMount() {
this.interval = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const { duration } = this.props;
let timeLeft = duration - this.state.seconds;
timeLeft = Number(timeLeft);
let minutes = Math.floor(timeLeft % 3600 / 60);
let seconds = Math.floor(timeLeft % 3600 % 60);
let minutesDisplay = minutes > 0 ? minutes + (minutes === 1 ? " : " : " : ") : "";
let secondsDisplay = seconds > 0 ? seconds + (seconds === 1 ? "" : "") : "";
return <p className="badge badge-success">Time Left: {minutesDisplay}{secondsDisplay}</p>;
}
}
export default Timer;
Quiz Component:
import React, { Component } from 'react';
import { ActionTypes } from '../redux/constants/actionTypes';
import Review from './Review';
import Questions from './Questions';
import Result from './Result';
import { connect } from 'react-redux';
// import { saveQuizAll } from '../commonjs/common.js';
const mapStateToProps = state => { return { ...state.quiz, ...state.mode, ...state.pager, ...state.quizAll } };
const mapDispatchToProps = dispatch => ({
onSubmit: payload => dispatch({ type: ActionTypes.QuizSubmit, payload }),
onQuizChange: payload => dispatch({ type: ActionTypes.QuizAnswerAll, payload }),
onPagerUpdate: payload => dispatch({ type: ActionTypes.PagerUpdate, payload })
});
class Quiz extends Component {
move = (e) => {
let id = e.target.id;
let index = 0;
if (id === 'first')
index = 0;
else if (id === 'prev')
index = this.props.pager.index - 1;
else if (id === 'next') {
index = this.props.pager.index + 1;
}
else if (id === 'last')
index = this.props.pager.count - 1;
else
index = parseInt(e.target.id, 10);
if (index >= 0 && index < this.props.pager.count) {
let pager = {
index: index,
size: 1,
count: this.props.pager.count
};
this.props.onPagerUpdate(pager);
}
}
saveStore(e) {
let allQuiz = [];
console.log(this, e);
allQuiz = this.props.saveAll(e.props.quizAll, e.props.quiz);
console.log(allQuiz);
this.props.onQuizChange(allQuiz);
}
setMode = (e) => this.props.onSubmit(e.target.id);
// setMode(e) {
// console.log('in mode',e);this.props.onSubmit(e.target.id);
// }
renderMode() {
console.log('Inside here', this.props.mode);
if (this.props.mode === 'quiz') {
return (<Questions move={this.move} />)
} else if (this.props.mode === 'review') {
return (<Review quiz={this.props.quiz} move={this.move} />)
} else {
console.log('Before Results');
const divSel = document.querySelector('div.col-6.text-right');
// console.log('divSel', divSel);
if (divSel) {
divSel.style.display = "none";
}
return (<Result questions={this.props.quizAll || []} />)
}
}
render() {
return (
<div>
{this.renderMode()}
{(this.props.mode !== 'submit') &&
<div>
<hr />
<button id="quiz" className="btn btn-primary" onClick={this.setMode}>Quiz</button>
<button id="review" className="btn btn-primary" onClick={this.setMode}>Review</button>
<button id="submit" className="btn btn-primary" onClick={(e) => {this.setMode(e); this.saveStore(this)}}>Submit Quiz</button >
</div >}
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Quiz);
I think you can have two approaches.
1. The "react" way
In the Parent component:
// ...
constructor(props) {
// ...
this.state = {
timeExpired: false
};
}
const onTimeExpired = () => {
this.setState({timeExpired: true});
}
// ...
render() {
return (
<div className="container">
{ // ... }
<Timer duration={900} onTimeExpired={onTimeExpired}/>
<Quiz quiz={this.state.quiz} quizId={this.state.quizId} saveAll={saveQuizAll} mode={this.state.mode} triggerSubmit={this.state.timeExpired} />
</div>
);
}
In the Timer component:
// ...
componentDidUpdate() {
if (this.state.seconds === this.props.duration) {
this.props.onTimeExpired();
}
}
// ...
In the Quiz component:
// ...
componentDidUpdate() {
if (this.props.triggerSubmit) {
// Do whatever you do on submit
}
}
// ...
2. The "quick and dirty" way:
In the Timer component
// ...
componentDidUpdate() {
if (this.state.seconds === this.props.duration) {
const quizForm = document.getElementById('quizFormId');
quizForm && quizForm.submit();
}
}
// ...
Provide a prop method onTimeFinished in your Timer component. Then in your render function you can add
{ !(this.props.duration-this.state.seconds) && this.props.onTimeFinished() }
Reference: React Conditional Rendering
try this:
Parent Component:
// state
state = {
triggerSubmit: false
}
// functions
doSubmit = () => {
this.setState({ triggerSubmit: true });
}
resetSubmit = () => {
this.setState({ triggerSubmit: false });
}
// jsx
<Timer duration={900} doSubmit={this.doSubmit} />
<Quiz
quiz={this.state.quiz}
quizId={this.state.quizId}
saveAll={saveQuizAll}
mode={this.state.mode}
resetSubmit={this.resetSubmit}
triggerSubmit={this.state.triggerSubmit} />
Timer Component:
// function
doSubmit = (timeLeft) => {
if (timeLeft === 0) {
this.props.doSubmit();
}
}
// jsx
<p className="badge badge-success"
onChange={() => {this.doSubmit(timeLeft)}>
Time Left: {minutesDisplay}{secondsDisplay}
</p>
Quiz Component:
// state
state = {
triggerSubmit: this.props.triggerSubmit
}
// function
triggerSubmit = () => {
if (this.state.triggerSubmit) {
your trigger submit code here...
this.props.resetSubmit();
}
}
I'm trying to receive fog effect in React. The main idea is that I have two components: first component handles with updating of coordinates of clouds and their velocity, the second component is responsible for one cloud. I have problem with moving of clouds, if I don't clear canvas I can see track of every cloud, if I apply canvas.clear I can't see anything. Do you have any tip, where I should place clear canvas.clear or do you have other ideas?
The first component:
import React from 'react';
import styled from 'styled-components';
import FogPiece from './Fog-Piece.jsx';
const CanvasContext = React.createContext();
const FogDiv = styled.div`
position: absolute;
width:100vw;
height:100vh;
`
class Fog extends React.Component{
constructor(props){
super(props);
this.canvas = React.createRef();
this.state = {
ctx: null,
parameters:[],
screenWidth : 0,
screenHeight: 0,
}
}
componentDidMount = () => {
Promise.all(this.newCoordinates()).then((paramArray) =>{
this.setState({
ctx: this.canvas.current.getContext('2d'),
screenWidth: this.canvas.current.parentNode.getBoundingClientRect().width,
screenHeight: this.canvas.current.parentNode.getBoundingClientRect().height,
parameters: paramArray
});
window.requestAnimationFrame(this.update)
})
}
newCoordinates = () => {
return(Array.from(Array(this.props.density).keys()).map(elem =>{
return new Promise (resolve => {
const params = {
x: this.random(0,this.state.screenWidth),
y: this.random(0,this.state.screenHeight),
velocityX: this.random(-this.props.maxVelocity, this.props.maxVelocity),
velocityY: this.random(-this.props.maxVelocity, this.props.maxVelocity)
}
resolve(params)
})
}))
}
updateCoordinates = () => {
return(this.state.parameters.map(elem =>{
return new Promise (resolve => {
elem = this.ifCross(elem.x, elem.y, elem.velocityX, elem.velocityY);
const params = {
x: elem.x + elem.velocityX,
y: elem.y + elem.velocityY,
velocityX: elem.velocityX,
velocityY: elem.velocityY
}
resolve(params)
})
}))
}
random = (min,max) => {
return Math.random()*(max - min) + min
}
ifCross = (x,y, velocityX, velocityY) => {
if (x > this.state.screenWidth){
x = this.state.screenWidth
velocityX = - velocityX
}
if (x < 0){
x = 0
velocityX = - velocityX
}
if (y > this.state.screenHeight){
y = this.state.screenHeight
velocityY = - velocityY
}
if (y < 0){
y = 0
velocityY = - velocityY
}
return {x:x, y:y, velocityX:velocityX, velocityY:velocityY }
}
update = () => {
Promise.all(this.updateCoordinates()).then((paramArray) =>{
//here is the problem
// this.state.ctx.clearRect(0,0,this.state.screenWidth, this.state.screenHeight)
this.setState({
parameters: paramArray,
});
window.requestAnimationFrame(this.update)
})
}
render(){
return(
<FogDiv>
<canvas width={this.state.screenWidth} height={this.state.screenHeight} ref = {this.canvas} >
{this.state.ctx && (
<CanvasContext.Provider value = {this.state.ctx}>
{this.state.parameters.map(param =>(
<FogPiece
x = {param.x}
y = {param.y}
/>
))}
</CanvasContext.Provider>
)}
</canvas>
</FogDiv>
)
}
}
export default Fog;
export {
CanvasContext
}
the second one:
import React from 'react';
import styled from 'styled-components';
import {CanvasContext} from './Fog.jsx';
class FogPiece extends React.Component{
constructor(props){
super(props);
this.state = {
image:'https://media.istockphoto.com/vectors/sample-red-square-grunge-textured-isolated-stamp-vector-id471401412',
}
}
random(min,max){
return Math.random()*(max - min) + min
}
render(){
return(
<CanvasContext.Consumer>
{ctx => {
console.log("x", "y", this.props)
const img = new Image();
img.src = this.state.image;
img.onload = () => {
ctx.drawImage(img,
this.props.x,
this.props.y,
40,
40)
}
}}
</CanvasContext.Consumer>
)
}
}
export default FogPiece;