Please, look at my code, I assign a new value to the variable in data and var width have value 100. After that, when animation end, i try return value to var width 100, and start animation again, but Vue does not assign new value 100 and stay 0. But if i will do this with setTimeout it's work perfect. Why is this not happening in nextTick?
link to jsfiddle
new Vue({
el: '#app',
data: {
width: 100,
time: 0
},
mounted() {
setTimeout(() => {
this.time = 5000;
this.width = 0;
setTimeout(() => {
this.rerenderBar();
}, 5100)
}, 1000)
},
methods: {
rerenderBar() {
this.time = 0;
this.width = 100;
/* this.$nextTick(() => {
this.time = 5000;
this.width = 0;
}) */
setTimeout(() => {
this.time = 5000;
this.width = 0;
}, 1500)
}
}
})
<div id="app">
<div class="progress-bar-wrap">
<div class="progress-bar" :style="{
'width': width + '%',
'transition-duration': `${time}ms`
}"></div>
</div>
</div>
My guess is that because $nextTick runs after Vue's DOM update cycle and your animations are powered by css transitions directly on the element (not handled by Vue), the $nextTick happens immediately after calling renderBar It does not wait for your animation to complete.
If you need to wait for the animation to finish, you can look into using Vue Transitions and use Javascript Hooks to reset the width of the bar when the animation finishes.
Related
ive got a component where i try to make some sort of magnetic snap scroll. i know the css usual css way of snap-y, and setting snap behavior to smooth, but the snap is too fast. so i tried on my own to create a component where if the app detects a scroll down or up with the function, and a scroll below
const [scrollDir, setScrollDir] = useState("");
const [index, setIndex] = useState("0")
useEffect(() => {
const threshold = 0;
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDir = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < threshold) {
ticking = false;
return;
}
if(scrollY>lastScrollY){
// setScrollDir("scrolling down")
scroller.scrollTo('about',{
duration: 1000,
delay: 100,
smooth: 'linear',
})
}
// setScrollDir(scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
window.addEventListener("scroll", onScroll);
console.log(scrollDir);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollDir]);
the magnetic scroll doesnt work, the page goes all jumpy. i could add a global css to make smooth scrolling, but that defeats the purpose of giving easein animation of scrolling. is there a way to work around this?
Hello
I am working on a timer in React to understand how Hooks works, and so far everything is ok except the start button (in my case the timer starts automatically and start button should be use with pause). I can't figure how to resolve this problem with these hooks.
const { useRef, useState, useEffect } = React;
function Minuteur() {
const intervalRef = useRef();
const [timer, setTimer] = useState(30);
useEffect(() => {
const id = setInterval(() => {
setTimer((oldTimer) => oldTimer - 1);
}, 1000);
intervalRef.current = id;
}, []);
const stopTimer = () => {
clearInterval(intervalRef.current);
};
const resetTimer = () => {
setTimer(30)
};
const playTimer = () => {
};
return (
<div>
<p>Il reste : {timer} secondes</p>
<button onClick={playTimer}> PLAY! </button>
<button onClick={stopTimer}> STOP! </button>
<button onClick={resetTimer}> RESET! </button>
</div>
);
Codepen
function Minuteur() {
// Définition de la référence
const intervalRef = useRef();
const [timer, setTimer] = useState(30);
const [timerRunning, setTimerRunning] = useState(false); // I added a state for if the timer should be running or not
useEffect(() => {
let interval = null;
if (timerRunning) { // Check if the timer is running
interval = setInterval(() => {
setTimer(timer => timer - 1);
}, 1000);
}
return () => clearInterval(interval);
}, [timerRunning]); // rerun side effect when timerRunning changes
// Fonction permettant d'arrêter le ‘timer’
const stopTimer = () => {
setTimerRunning(false) // Set running to false
};
const resetTimer = () => {
setTimer(30);
stopTimer();
};
const playTimer = () => {
setTimerRunning(true); // set running to true
};
...
}
Edit: Everything in the [] dependency array at the end of the useEffect hook is what the side effect "watches". So by adding the timerRunning to the dependency array the useEffect hook will watch for the timerRunning and when it changes, it will cause the hook to re-render. If it is an empty array then it will only ever run on the initial load. That is why your timer started on refresh.
My countdown timer won’t stop after 0 and it went to negative even after I clear the interval. I seem not able to see where it went wrong.
Also after the timer goes to 0, I want the page automatically go to the next page without giving specific route, so I’m thinking using useHistory and goForward() but don’t know where I add the hook in this function. Can I return clearInterval and history.goForward() both?
import React, { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
const Timer = () => {
const [seconds, setSeconds] = useState(10);
const history = useHistory();
useEffect(() => {
const interval = setInterval(
() => setSeconds((prevTimer) => prevTimer - 1),
1000,
);
if (seconds === 0) {
return () => clearInterval(interval);
}
}, []);
return <div className="countdown">{seconds}</div>;
};
export default Timer;
Your useEffect function is only called once on first render and never again. So you don't actually clear the interval. clearInterval needs to sit outside of that function so that it can be called when the seconds reach zero. I would write your code like so:
export default function App() {
const [seconds, setSeconds] = React.useState(10);
const interval = React.useRef();
React.useEffect(() => {
interval.current = setInterval(
() => setSeconds((prevTimer) => prevTimer - 1),
1000
);
}, []);
if (seconds === 0) {
clearInterval(interval.current);
}
return <div className="countdown">{seconds}</div>;
}
useRef is like a “box” that can hold a mutable value in its .current property.
So here you assign the interval to it and can clear it any time you want.
Sandbox
when you set timer inside useEffect it's not aware of changes. I have updated your code. codepen
export default function App() {
let [seconds, setSeconds] = useState(15),
[timer, setTimer] = useState(null); // IN YOU NEED TO STOP TIMER
useEffect(() => {
if (seconds > 0) updateSeconds();
else {
// go to next page
// set timer
// setSeconds(15);
}
// eslint-disable-next-line
}, [seconds]);
/* Timer Logic */
function updateSeconds() {
let timeOut = setTimeout(() => {
setSeconds(seconds - 1);
}, 1000);
setTimer(timeOut);
}
return (
<div className="App">
<h1>Hello Timer</h1>
<h2>{seconds}</h2>
</div>
);
}
I'm coding a greeting card generator to train in VueJS 3. Everything is working correctly, apart from one thing, look at my code:
<template>
<div>
<h1>greeting card generator</h1>
<div class="board">
<canvas id='myCanvas' :width="size.w" :height="size.h" tabindex='0'
style="border:1px solid #000000;"
></canvas>
</div>
<textarea
:style="'width:' + size.w + 'px; resize:none;'"
v-model="texte"
placeholder="Write your text here">
</textarea>
</div>
</template>
<script>
import {
defineComponent, onMounted, ref, reactive, watch,
} from 'vue';
export default defineComponent({
setup() {
const myCanvas = ref(null);
const texte = ref('');
const rapport = ref(0);
const size = reactive({
w: window.innerWidth * 0.8,
h: (window.innerWidth * 0.8) / 1.8083832335329342,
});
function drawText() {
const fontSize = 0.05 * window.innerWidth - 10;
myCanvas.value.font = `${fontSize}px Adrip`;
myCanvas.value.textAlign = 'center';
const x = size.w / 2;
const lineHeight = fontSize;
const lines = texte.value.split('\n');
for (let i = 0; i < lines.length; i += 1) {
myCanvas.value.fillText(
lines[lines.length - i - 1],
x,
(size.h * 0.98) - (i * lineHeight),
);
}
}
function initCarte() {
const background = new Image();
background.src = '/img/fond.jpeg';
background.onload = function () {
rapport.value = background.naturalWidth / background.naturalHeight;
size.h = size.w / rapport.value;
try {
myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
} catch (e) {
console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
}
drawText();
};
}
function handleResize() {
size.w = window.innerWidth * 0.8;
size.h = size.w / rapport.value;
initCarte();
}
window.addEventListener('resize', handleResize);
onMounted(() => {
const c = document.getElementById('myCanvas');
const ctx = c.getContext('2d');
myCanvas.value = ctx;
initCarte();
});
watch(texte, () => {
initCarte();
});
return {
myCanvas,
size,
texte,
};
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#font-face {
font-family: 'Adrip';
src: local('Adrip'), url('/fonts/adrip1.ttf') format('truetype');
}
#myCanvas {
border: 1px solid grey;
}
</style>
Look at this line:
h: (window.innerWidth * 0.8) / 1.8083832335329342,
If I don't hardcode this and only put the canonical value window.innerWidth * 0.8, the image doesn't display, although the size.h = size.w / rapport.value; line executes correctly.
I really don't understand this behaviour, could somebody explain it to me?
Also, if anybody has a clue on how it would be possible to load the image once and for all so that I don't have to load it at every refresh, it would be better :)
Thanks in advance!
Your problem is that you change the size of the canvas after drawing the image, due to how templating magic works. If you put debugger behind drawText(); in the background onload function, you will see that it actually draws the image. However, in this same function, you set size.h. size is reactive, and is thus marked as "dirty". size is also used in the template, so the template is marked dirty. After the onload function is executed, Vue will rerender your template... and erase your image.
I think your best bet here is to use nextTick. You need to use it sparingly, but I think this is one of the instances where you have no choice but to wait for the DOM to settle. To do this, import nextTick from vue:
import { nextTick } from 'vue';
Then surround your drawImage try-catch block with that.
background.onload = function () {
rapport.value = background.naturalWidth / background.naturalHeight;
size.h = size.w / rapport.value;
nextTick(() => {
try {
myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
} catch (e) {
console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
}
drawText();
});
};
As for your last question how to load the image once... the short answer is... you can't. Whenever the canvas changes, you need to redraw it. At least the image should be cached by the browser, so it just draws it from cache rather than doing another http request.
On clicking a button, I will get a dropdown with a list of items. I want to check how much time it takes for the drop down to come after clicking the button. I want to write a test using cypress for this to check performance. Intention is to check the performance of the app when the item list contains 100 thousands of values.
you can try something like this:
index2.html:
<div class="many">
</div>
<button id="manyadd">add many</button>
<script>
var prom = () => new Promise(resolve => {
var parent = document.querySelector(".many");
var e = document.createElement("div");
e.classList.add("red");
parent.append(e);
setTimeout(resolve, 1000);
})
async function createElement() {
for(var i = 0; i < 10; i++) {
await prom();
}
}
document.querySelector("#manyadd").addEventListener("click", () => createElement());
</script>
<style>
.red {
width: 100px;
height: 100px;
background-color: red;
}
</style>
and cypress code:
describe("many elements", () => {
beforeEach(() => {
cy.visit("index2.html");
})
it("wait until all elements", () => {
let t1 = null;
let t2 = null;
cy.get("#manyadd").then(b => b.click());
cy.get(".many > div").then(() => t1 = new Date());
cy
.get(".many > div", { timeout: 20000 })
.should(elements => {
expect(elements.length).to.eq(10);
t2 = new Date();
})
.then(() => cy.log(`duration: ${(t2.getTime()-t1.getTime())/1000} seconds`))
})
})
The example app code adds 10 div container and the cypress test waits until all are loaded. if you want exact times you may utilize MutationObserver but this is definitly more code than in my example :-P
Result: