Context
Newer to React; I have a parent component that is generating anywhere from 1-6 youtube videos, using react-youtube components. The Idea is that the YouTube components will be generated dynamically based on youtube video ids in an array. Currently, I'm generating the components like
{this.state.videoIds.map((videoId, index) =>
<YouTube
key={index}
opts={opts}
videoId={videoId}
onReady={this._onReady} />
)}
As far as generating the components, it's working as expected. However, the issue is, when this.state.videoIds gets updated, it causes all of the YouTube components to be refreshed. That behavior makes sense to me.
Issue
The issue I have is the following: If the functionality stays the same, I need a way to be able to track each of the YouTube components player time, so that if a refresh happens, I can pass that data back down to the components when they are refreshed so they are able to be played from where they left off.
So my question is, is there a way to dynamically generate these components without needing to refresh all of the existing components?
For what it's worth, I have thought about just having a parent component that will statically add the components for each use case, but that seems awfully clunky, and I don't really want to go that route.
Edit
This feels relevant, I'm divvying up the screen space evenly based on how many players are loaded at any given time, code looks like the following:
calculateVideoWidth = () => {
if (this.state.videoIds.length <= 3) {
return (document.body.clientWidth - 15) / this.state.videoIds.length;
} else {
return (document.body.clientWidth - 15) / this.MAX_NUMBER_OF_COLUMNS;
}
};
calculateVideoHeight = () => {
if (this.state.videoIds.length <= 3) {
return window.innerHeight - 4;
} else {
return (window.innerHeight - 4) / this.MAX_NUMBER_OF_ROWS;
}
};
In my render method, I'm doing the following:
let opts = {
height: this.calculateVideoHeight(),
width: this.calculateVideoWidth()
};
After looking at #Tholle's comment, it occurs to me that the 'reload' is coming from the resizing. Which I guess makes sense. There has to be a better way to do this?
Edit 1
Check out this link to see what I am experiencing: Video App. In the input box, just add a video id separated by a ','.
This code might help you to start video from wherever user left.
The thing is we need to maintain or store the last played state of video wherever user left, some where permanently not erasable on reload, so here implemented using localStorage.
import React from "react";
import ReactDOM from "react-dom";
import YouTube from "react-`enter code here`youtube";
class App extends React.Component {
state = {
videoIds: ["2g811Eo7K8U"],
videosPlayState: {}
};
componentDidMount() {
let videosPlayState = localStorage.getItem("videosPlayState") || "{}";
this.setState({
videosPlayState: JSON.parse(videosPlayState)
});
}
addVideo = () => {
this.setState(prevState => ({
videoIds: [...prevState.videoIds, "9kBACkguBR0"]
}));
};
_onStateChange = (event, videoId) => {
let newPlayState = { ...this.state.videosPlayState
};
newPlayState[videoId] = event.target.getCurrentTime();
localStorage.setItem("videosPlayState", JSON.stringify(newPlayState));
};
_onReady(event, videoId) {
event.target.seekTo(this.state.videosPlayState[videoId] || 0);
}
render() {
const opts = {
height: "200",
width: "400",
playerVars: {
autoplay: 1
}
};
return ( <
div className = "App" >
<
div className = "video-container" > {
this.state.videoIds.map((videoId, index) => ( <
YouTube onStateChange = {
event => {
this._onStateChange(event, videoId);
}
}
onReady = {
event => {
this._onReady(event, videoId);
}
}
key = {
index
}
videoId = {
videoId
}
opts = {
opts
}
className = {
"item"
}
/>
))
} <
/div> <
div >
<
button onClick = {
this.addVideo
} > Add video < /button> <
/div> <
/div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
here is the css:
.video-container .item {
padding: 25px;
display: inline-block;
}
.video-container {
display: flex;
flex-wrap: wrap;
}
here is a link might help you enter link description here
After playing with flexbox a bit more, I was able to solve this issue. I ended up with the following:
html, body, #root, #root > div, div.container, iframe {
width: 100%;
height: 100%;
margin: 0;
}
div.container {
display: flex;
flex-wrap: wrap;
}
span {
flex-basis: 33%;
flex-grow: 1;
}
For reference, here is my jsx
<div>
<div className="container">
{this.state.videoIds.map((videoId, index) =>
<YouTube
id="flex-item"
key={index}
videoId={videoId}
onStateChange={this._onStateChange}
onReady={this._onReady}/>
)}
</div>
</div>
I figured this could be done with flexbox, but I hadn't come up with the solution for it yet. I hope this helps someone else out.
Related
Mobile touch-screen scrolling works really smoothly out of the box in modern browsers. For example, if you swipe fast across the div, it keeps scrolling for a little while, gradually reducing its' speed, after you release your finger from the screen. This way, you can reach the desired div scroll position a lot more efficiently.
Here's my snippet with a simple div consisting of 50 child items:
const App = () => {
let data = []
for (let i = 0; i < 50; i++) {
data.push({
key: i,
value: `item ${i}`
})
}
const renderItem = (x) => {
return (
'hello' + x.key
)
}
return ( <
div className = "tileView" > {
data.map((x, i) => {
return ( <
div key = {
i
}
className = "item" > {
renderItem(x)
} <
/div>
);
})
} <
/div>
);
};
ReactDOM.render( < App / > , document.getElementById('root'));
.tileView {
display: flex;
overflow-x: scroll;
overflow-y: hidden;
width: 600px;
}
.item {
border: 1px solid gray;
width: 100px;
height: 100px;
flex-shrink: 0;
margin: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I want to find a solution that would make that div scrollable on mouse drag, matching the behavior of touch and drag scrolling.
I have tried implementing this solution. It's working, however, it's not perfect, as the page scroll immediately stops on the mouse-up event.
Ideally, I'm looking for a solution that would implement mobile scrolling behavior on desktops.
Not sure if I'm thinking in the right direction, but maybe it's possible to add an event to a div that would make the browser think that the div is touched rather than clicked?
If not, what would be the best approach to resolve this?
I think what you're looking for is momentum based drag scrolling.
You can see a good example in the codepen provided in the answer to this question. Hopefully that helps :)
hey if anybody could help me with the next step in my project, Id really appreciate it. its a very simple react flashcard app that takes 10 questions from a trivia api and displays all 10 of them at once to the screen, each card with the question and answer choices, and when the user clicks each card, the card does a simple flip animation that shows the correct answer on the back. my next challenge is to have the user attempt to get the question right by clicking on one of the answer choices and if they are wrong, it will say they are wrong until they get it right it will then flip as it did before to show the right answer. basically I just want to learn how I can make it a little bit more interactive for the user.
my app.js file maps over the dictionary objects from my api and turns them into props. Here is the code in that file:
import React, { useState, useEffect } from "react"
import FlashcardList from "./FlashcardList"
import './app.css'
import axios from 'axios' // makes importing from api easy
export default function App() {
const [flashcards, setFlashcards] = useState([])
useEffect(() => {
axios.get('http://127.0.0.1:5000/')
.then(res => {
setFlashcards(res.data.results.map((questionItem, index) => { // mapping over api to get objects "questionItem" and the index of each one
const answer = decodeString(questionItem.correct_answer) // setting the correct_answer objects from api to answer
const options = [
...questionItem.incorrect_answers.map(a => decodeString(a)), answer // spreading incorrect_answers objects into an array with answer at the back to set all of them into options
]
return {
id: `&{index}-${Date.now()}`, // sets the id to the index from the api and the exact time to make sure its always unique
question: decodeString(questionItem.question), // setting question objects from api to question
answer: answer, // already defined above
options: options.sort(() => Math.random() - .5) // sorting all the options randomly
}
}))
})
}, [])
function decodeString(str) {
const textArea = document.createElement('textarea')
textArea.innerHTML= str
return textArea.value // function brings all of our objects into this new element and decodes all of the encoded html
}
return (
<div className="container">
<FlashcardList flashcards={flashcards} />
</div>
)
}
my flashcard.js file recieves the props from the app.js file and builds the front and back of the card, creates a useState to make the card flip, and a useState to calculate the height for each card. this is the code on the flashcard.js file:
import React, { useState, useEffect, useRef } from 'react'
export default function Flashcard({ flashcard }) { // recieving flashcard prop
from our mapping in flashcardlist.js, each w a unique id
const [flip, setFlip] = useState(false)
const [height, setHeight] = useState('initial') //sets the state for our initial height to be replaced by the max height
const frontEl = useRef() // lets us have a reference from the front and back through every rerendering of them
const backEl = useRef()
function setMaxHeight() {
const frontHeight = frontEl.current.getBoundingClientRect().height //gives us dimensions of the rectangle but we only need the height
const backHeight = backEl.current.getBoundingClientRect().height
setHeight(Math.max(frontHeight, backHeight, 100)) // sets the height (setHeight) to the maximum height of front or back but the minimum is 100px
}
useEffect(setMaxHeight, [flashcard.question, flashcard.answer, flashcard.options]) //anytime any of these change then the setMaxHeight will change
useEffect(() => {
window.addEventListener('resize', setMaxHeight) //everytime we resize our browser, it sets the max height again
return () => window.removeEventListener('resize', setMaxHeight) //removes the eventlistener when component destroys itself
}, [])
return (
<div
className={`card ${flip ? 'flip' : ''}`} // if flip is true classname will be card and flip, if flip isnt true it will just be card
style={{ height: height }} //setting height to the variable height
onClick={() => setFlip(!flip)} // click changes it from flip to non flip
>
<div className='front' ref={frontEl}>
{flashcard.question}
<div className='flashcard-options'>
{flashcard.options.map(option => {
return <div className='flashcard-option'>{option}</div>
})}
</div>
</div>
<div className='back' ref={backEl}>{flashcard.answer}</div>
</div>
)
}
// setting the front to show the question and the answers by looping through the options to make them each an option with a class name to style
// back shows the answer
my flashcardList.js file takes in the flashcards props from app.js and gives them each a unique id before passing it to flashcard.js to format the cards. here is that code:
import React from 'react'
import Flashcard from './Flashcard'
export default function FLashcardList({ flashcards }) {
// taking in the flashcards as destructured props so we dont have to make a props. variable
return (
// card-grid is a container so we can put all the cards in a grid to ensure they change in size proportionally to the size of the window //
<div className='card-grid'>
{flashcards.map(flashcard => { // loops through the flashcards api and maps each one to flashcard
return <Flashcard flashcard={flashcard} key={flashcard.id} /> // each flashcard is then passed down to the "Flashcard.js" component we created returned w a unique id
})}
</div>
)
}
and then i have a app.css file that handles the complications of making the flip animation:
body {
background-color: #c8d0d2;
margin: 0;
}
.container {
max-width: 900px;
margin: 1rem 2rem;
}
.card-grid {
display: grid;
align-items: center;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* sets the cards to
dynamically fit the page no matter how big it gets and to 200px minimum each card*/
gap: 1rem;
}
.card {
display: flex;
justify-content: center;
align-items: center;
position: relative;
border-radius: .25rem;
box-shadow: 0 0 5px 2px rgba(0, 0, 0, .3);
background-color: white;
transform-style: preserve-3d; /* to preserve our 3d elements so the card actually
flips to the back instead of just rotating the front 180 deg */
/* height: 150px; /* this will be changed eventually so the height is automatically
set by the size of the content text */
transition: 150ms; /* so we can see the flip animation by slowing it down */
cursor: pointer;
transform:perspective(1000px) /* to make the flip seem a little more 3d, less flat
*/rotateY(var(--rotate-y, 0)) /* card will flip to whatever we set the rotate-y variable in .card.flip */
translateY(var(--translate-y, 0));
}
.card:hover {
box-shadow: 0 0 5px 2px rgba(0, 0, 0, .6); /* makes the outline darker when hover */
--translate-y: -2px; /* moves the card up when hover */
}
.card.flip {
--rotate-y: 180deg;
}
.card .front {
left: 0;
}
.card .front,
.card .back {
position: absolute; /* so the front and back dont offset eachother */
padding: 1rem;
backface-visibility: hidden; /* makes it so anything facing backwards is hidden so
the back of the card isnt showing on the front and the front isn't showing when the
card is flipped to the back */
}
.card .back {
transform: rotateY(180deg); /* needs to be rotated because it's on the back so it
will be flipped to the front and made right side up when flipped */
}
.flashcard-options {
margin-top: .5rem;
}
.flashcard-option {
margin-top: .25rem;
color: #555;
font-size: .75rem;
}
.flashcard-option:first-child {
margin-top: 0;
}
I'm a beginner and i appreciate any help. Thank you!
I'm not exactly sure what you're having difficulty with, since it seems your code is set up pretty close to what you want (I haven't run it or anything), but seems like you just want to add a click event to where you're listing your options.
<div className='flashcard-options'>
{flashcard.options.map(option => {
return <div onClick={() => handleUserPicksAnswer(option)} className='flashcard-option'>{option}</div>
})}
</div>
From there just write the logic for the trivia game inside handleUserPicksAnswer. Something similar to the following.
function Flashcard(props) {
// other stuff
const [incorrect, setIncorrect] = useState(0)
const [flip, setFlip] = useState(false)
const handleUserPicksAnswer = (answer) => {
if(answer.is_correct) {
// display correct prompt - whatever you want
return
}
if(incorrect + 1 === MAX_TRIES) {
setFlip(true)
}
setIncorrect(incorrect + 1)
}
return <div>
{// other stuff }
<div className='flashcard-options'>
{flashcard.options.map(option => {
return <div onClick={() => handleUserPicksAnswer(option)}
className='flashcard-option'
>{option}</div>
})}
</div>
</div>
</div>
}
Something approximate to that.
I am making the clone of a webpage which is made in JS but I am developing it by HTML, CSS, JS. Its navBar looks like this . Here is the link if you want to experience yourself link.
So, I have tried to implement this using IntersectionObserver API as well as by using window.addEventListener(). I don't want to implement this by using scroll event Listener because it is too heavy for end user.
const intersectionCB = ([entry]) => {
const elem = entry.target;
if (!entry.isIntersecting) {
elem.classList.add('nav__2-sticky');
// observer.unobserve(navBar);
} else {
elem.classList.remove('nav__2-sticky');
}
};
const observer = new IntersectionObserver(intersectionCB, {
root: null,
threshold: 0
});
observer.observe(navBar);
In HTML file
<div class="nav__2">
<div class="row nav__2--content">
<div class="logo-container">
<img src="img/logo-black.png" alt="" class="logo" />
</div>
........
In SCSS file
.nav {
&__2 {
top: 8rem;
position: absolute;
left: 0;
width: 100%;
&-sticky {
position: fixed;
top: 0;
}
}
}
You might understand what is happening. When navBar gets out of the view, (navBar is positioned at 8rem from top!). I append nav__2-sticky class (which is positioned fixed at 0 from top) to appear on the screen. Due to which entry.isIntersecting becomes true and elem.classList.remove('nav__2-sticky'); is executed. As a result navBar again gets out of the view and again elem.classList.add('nav__2-sticky') is executed. This cycle of adding and removing classes due to entry.isIntersecting becoming True and False is creating a problem for me. This happens in such speed that it shows abnormal behaviour.
So, is there any proper solution for this? I would also like to hear other solutions that might work.
I used scroll event after all. Here is the code, I think I don't need to explain. You will get more detailed explanation here link
const initialCords = navBar.getBoundingClientRect();
document.addEventListener('scroll', () => {
if (window.scrollY > initialCords.top) {
navBar.classList.add('nav__2-sticky');
} else {
navBar.classList.remove('nav__2-sticky');
}
});
Another angle could be to run the intersection observer on an element that is out of view (below the bottom of the screen) and not only the navbar itself
In a functional react component, I'm trying to check whether a call to action button (a different component) is within the viewport. If it's not, I want to display a fixed call to action button at the bottom of the viewport, which shows/hides, depending on whether the other button is visible.
I can do this using a combination of Javascript and react hooks, but although the code works in some components in my app, it doesn't work in others; I'm guessing due to react lifecycles.
I'm also aware that this is NOT the way I should be doing things in react, so would prefer to achieve the same result, but in a proper 'react way'.
I've been looking at using refs, but ideally wanted to avoid having to change my functional component to a class, as I'd like to use react hooks for the show/hide of the fixed cta. However, if this is a requirement in order to get the functionality I want, I could go for that.
Here's what I've got so far - basically, I want to replace document.querySelector with a react method:
useEffect(() => {
const CTA = document.querySelector('#CTANextSteps');
const ApplyStyle = () => (isInViewport(CTA) ? setVisible(false) : setVisible(true));
ApplyStyle();
window.addEventListener('scroll', ApplyStyle);
window.addEventListener('resize', ApplyStyle);
return function cleanup() {
window.removeEventListener('scroll', ApplyStyle);
window.removeEventListener('resize', ApplyStyle);
};
});
const isInViewport = (elem) => {
const bounding = elem.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
As mentioned above, this function works in some areas of the app without issue, but doesn't in others; I get a Cannot read property 'getBoundingClientRect' of null error. I was surprised it worked at all, but rather than tinkering with it to try and get it working everywhere, I want to rewrite it properly.
As always, any assistance would be much appreciated. Thanks.
I was able to do it with the depedency react-visibility-sensor#5.1.1
I followed the tutorial in this link and it worked fine with me.
I don't know if this is the correct way to do it, but it works!
Here is the link https://www.digitalocean.com/community/tutorials/react-components-viewport-react-visibility-sensor
I'll put an example just in case the previous link ever goes out.
import React, { Component } from 'react';
import VisibilitySensor from 'react-visibility-sensor';
class VisibilitySensorImage extends Component {
state = {
visibility: false
}
render() {
return (
<VisibilitySensor
onChange={(isVisible) => {
this.setState({visibility: isVisible})
}}
>
<img
alt={this.props.alt}
src={this.props.src}
style={{
display: 'block',
maxWidth: '100%',
width: '100%',
height: 'auto',
opacity: this.state.visibility ? 1 : 0.25,
transition: 'opacity 500ms linear'
}}
/>
</VisibilitySensor>
);
}
}
export default VisibilitySensorImage;
Site: http://blieque.comli.com/motion
Before I start, I know there are many alternatives to Lightbox2 (Lokesh Dhakar's) to display videos but I want to avoid using three different JavaScript things, already using MooTools and JQuery as I'd like to keep HTTP requests to a minimum, as well as disk usage.
As it comes, Lightbox2 does not support videos, full stop. However, I noticed that the JavaScript was essentially taking the contents of an a's href attribute and placing it in an img's src attribute when the light-box was open. As far as I can see, changing this img to an iframe (done) and setting the anchor tag's href to youtube.com/embed/*video-id* should generate an iframe containing that YouTube video (replacing watch?id= with embed/ presents a full screen version of the video.
I then also added JavaScript for width, height and frameborder attributes on top of the default class="lb-image". Now when the page is loaded and the Lightbox called it creates an empty window. If you inspect the code you can see all the attributes are there but the page in the frame frame isn't loaded, just creating an empty head and body tag.
I was just wondering if it was a server problem or a code problem and if so, how to fix it. If there any way to make it work?
Thanks
Note: I'm NOT using Drupal so the lightvideo option is not available.
Maybe someone still is looking for solution.
I had same problem in my project. Not with youtube iframe but it’s not difficult to implenet it. Lightbox2 cannot be extended so i wrote simple class whose adding listener and observer. For proper displaing require is that video have a poster with the same sizes. That’s the fastest way to keep corret sizes of popup.
In href required is to add dataset href with image url
<a href="POSTER_URL" data-href="VIDEO_URL" data-lightbox="Videos">
Open Lightbox
</a>
SCSS to cover image in popup and set fade effect while loading
.lightbox {
.lb {
&-outerContainer {
video {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 9999;
width: 100%;
height: auto;
opacity: 1;
transition: opacity 300ms ease-in-out;
border: none;
outline: none;
&:hover, &:focus {
border: none;
outline: none;
}
}
&.animating {
video {
opacity: 0;
}
}
}
&-container {
position: relative;
.lb-image {
border: none;
}
}
}
}
And JS class which one create and set video into popup. Maybe little messy but i don't care. It's only quick solution.
class LightBoxVideo {
constructor() {
this.videos = {};
this.lightBoxVideo();
}
lightBoxVideo = () => {
this.setEvents();
this.setMutationObserver();
}
setMutationObserver = () => {
const observer = new MutationObserver(mutation => {
const imageMutations = mutation.filter((m) => {
return m.attributeName === "src" && m.target.className === 'lb-image'
});
const overlayDisplay = window.getComputedStyle(document.querySelector('.lightboxOverlay'), null).display;
if("none" === overlayDisplay) {
this.removeVideoElement();
}
if(imageMutations.length > 0) {
if(this.videos[imageMutations[0].target.src]) {
this.removeVideoElement();
this.setVideoElement(this.videos[imageMutations[0].target.src]);
}
}
});
observer.observe(document.body, {
childList: false,
attributes: true,
subtree: true,
characterData: false
});
}
setEvents = () => {
const videoLinks = this.findVideoLinks();
videoLinks.forEach((link) => {
this.videos[link.href] = link;
link.addEventListener('click', (e) => {
this.removeVideoElement();
this.setVideoElement(e.target);
});
});
}
setVideoElement = (element) => {
const lightbox = document.querySelector('.lightbox')
const container = lightbox.querySelector('.lb-container');
const videoElement = this.createVideoElement(element);
container.prepend(videoElement);
}
removeVideoElement = () => {
const lightbox = document.querySelector('.lightbox')
const container = lightbox.querySelector('.lb-container');
const video = container.querySelector('video');
if(video) {
container.removeChild(video);
}
}
createVideoElement = (element) => {
const video = document.createElement('video');
video.setAttribute('poster', element.href);
video.setAttribute('controls', 'true');
const source = document.createElement('source');
source.setAttribute('src', element.dataset.href);
source.setAttribute('type', 'video/mp4');
video.append(source);
return video;
}
findVideoLinks = () => {
const hrefs = document.querySelectorAll('a[data-lightbox]');
const regex = /\.(mp4|mov|flv|wmv)$/;
if(0 === hrefs.length) {
return [];
}
return Array.from(hrefs).filter((href) => {
return !! href.dataset.href.match(regex);
});
}
}
To preview how it's work - codepen here: https://codepen.io/PatrykN/pen/RwKpwMe
Enabling video support
By default, support for videos is disabled. However, this can be easily enabled by checking the enable video support option on admin/settings/lightbox2.
Basic Example
This Video
Reference