I used transition in vue by watching current value in vuex using 'alertTip' component to achieve fade-in and fade-out animation, but sometimes it works and sometimes it doesn't work. When value changed and the function in watch doesn't work, the 'tips' can't disappear and shows all the time.
How to resolve this problem?
If there has other method to achieve fade-in and fade-out animation without jQuery.
Here is the code of the 'alertTip' component:
<template>
<transition name="slide-fade">
<div class="info-log" v-if="alertInfo.show">
<img src="../assets/success-icon.png" alt="success-icon" v-if="alertInfo.success">
<img src="../assets/error-icon.png" alt="success-icon" v-else>
<span class="info-text">{{alertInfo.alertText}}</span>
</div>
</transition>
</template>
<script>
import {mapGetters, mapActions} from 'vuex'
export default {
data () {
return {
value: ''
}
},
props: [],
mounted () {
},
methods: {
...mapActions('global', [
'_ChangeAlertInfo'
]),
showTip () {
let self = this
this.show = !this.show
setTimeout(function () {
self.show = !self.show
}, 1300)
}
},
computed: {
...mapGetters('global', [
'alertInfo'
])
},
watch: {
'alertInfo.show': {
deep: true,
handler (curVal, oldVal) {
let self = this
setTimeout(function () {
self._ChangeAlertInfo({
'show': false
})
}, 1300)
}
}
}
}
</script>
<style lang="less" scoped>
.slide-fade-enter-active {
transition: all .5s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-active {
opacity: 0;
}
.info-log {
position: fixed;
top: 40%;
left: 50%;
z-index: 1111;
margin-left: -92px;
min-width: 184px;
height: 60px;
border-radius: 5px;
padding-top: 20px;
text-align: center;
color: #FFF;
background-color: rgba(0,0,0,.5);
/* IE8 */
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#7f000000,endColorstr=#7f000000);
.info-text {
vertical-align: top;
}
}
</style>
Related
This is my first time using react and I have somewhat limited experience with javascript in general. Trying to adapt a project I found on codepen for my project. The idea being creating a dynamic graded quiz that returns some output if a user selects the wrong answer.
This is what I have as my html:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="{% static 'second/css/app/quiz_control.css' %}">
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<script src="{% static 'second/js/app/quiz_control.js' %}" defer></script>
</head>
<main class="container">
<section id="riots-to-renaissance">
</section>
</main>
</html>
javascript:
const RawHTML = (props) => <span dangerouslySetInnerHTML={{__html: props.html}}></span>;
class QuestionImage extends React.Component {
constructor(props) {
super(props);
this.imgRef = React.createRef();
}
componentDidUpdate(prevProps, prevState) {
if (this.imgRef.current && prevProps.img.src !== this.props.img.src) {
this.imgRef.current.classList.add('fade-in');
let timer = setTimeout(() => {
this.imgRef.current.classList.remove('fade-in');
clearTimeout(timer);
}, 1000)
}
}
render() {
return (
<img ref={this.imgRef} className="img-fluid" src={this.props.img.src} alt={this.props.img.alt} />
);
}
}
const QuizProgress = (props) => {
return (
<div className="progress">
<p className="counter">
<span>Question {props.currentQuestion+1} of {props.questionLength}</span>
</p>
<div className="progress-bar" style={{'width': ((props.currentQuestion+1) / props.questionLength) * 100 + '%'}}></div>
</div>
);
}
const Results = (props) => {
return (
<div className="results fade-in">
<h1>Your score: {((props.correct/props.questionLength) * 100).toFixed()}%</h1>
<button type="button" onClick={props.startOver}>Try again <i className="fas fa-redo"></i></button>
</div>
);
}
class Quiz extends React.Component {
constructor(props) {
super(props);
this.updateAnswer = this.updateAnswer.bind(this);
this.checkAnswer = this.checkAnswer.bind(this);
this.nextQuestion = this.nextQuestion.bind(this);
this.getResults = this.getResults.bind(this);
this.startOver = this.startOver.bind(this);
this.state = {
currentQuestion: 0,
correct: 0,
inProgress: true,
questions: [{
question: "<em>A Raisin in the Sun</em> was the first play by an African-American woman to be produced on Broadway. Who was the playwright?",
options: [{
option: "Lorraine Hansberry",
correct: true
}, {
option: "Maya Angelou",
correct: false
}],
img: {
src: 'https://interactive.wttw.com/sites/default/files/dusable-to-obama_raisin-in-the-sun.jpg',
alt: 'Characters in A Raisin in the Sun'
},
feedback: "Lorraine Hansberry's (1930–1965) play opened in 1959 to critical acclaim and was a huge success. The play is about a black family who faces racism when moving into an all-white suburb. The play is drawn from a similar experience in Hansberry’s early life.",
moreUrl: 'https://interactive.wttw.com/dusable-to-obama/hansberrys-victory'
}, {
question: "The internationally famous Harlem Globetrotters basketball team started with players from which Chicago High School?",
options: [{
option: "Wendell Phillips High School",
correct: true
}, {
option: "DuSable High School",
correct: false
}],
img: {
src: 'https://interactive.wttw.com/sites/default/files/dusable/riots_renaissance_thumb_5.jpg',
alt: 'A Harlem Globetrotter holding a basketball in each hand'
},
feedback: "The athletes who would become Harlem Globetrotters first played together as students at Wendell Phillips High School on the south side of Chicago. Later, they played as a team under the banner of the South Side's Giles Post of the American Legion and then as the Savoy Big Five before taking on their current name. The team was based in Chicago for 50 years, from 1926 through 1976.",
moreUrl: 'https://interactive.wttw.com/dusable-to-obama/harlem-globetrotters'
}, {
question: "What Chicagoan is known as the father of Gospel Music?",
options: [{
option: "Thomas A. Dorsey",
correct: true
}, {
option: "Langston Hughes",
correct: false
}],
img: {
src: 'https://interactive.wttw.com/sites/default/files/dusable-to-obama_thomas-dorsey.jpg',
alt: 'Thomas Andrew Dorsey'
},
feedback: "Some.",
moreUrl: 'https://interactive.wttw.com/dusable-to-obama/dorseys-gospel'
}, {
question: "Which of these African-American women ran for the office of president of the United States?",
options: [{
option: "All of the above",
correct: true
}, {
option: "None of the above",
correct: false
}],
img: {
src: 'https://interactive.wttw.com/sites/default/files/dusable/achieving_dream_thumb_9.jpg',
alt: 'Carol Moseley-Braun'
},
feedback: "more text.\"",
moreUrl: 'https://interactive.wttw.com/dusable-to-obama/carol-moseley-braun'
}, {
question: "Who was Oscar Stanton De Priest?",
options: [{
option: "The first Catholic priest in Chicago",
correct: false
}, {
option: "A United States congressman",
correct: true
}],
img: {
src: 'https://interactive.wttw.com/sites/default/files/dusable-to-obama_oscar-stanton-de-priest.jpg',
alt: 'Oscar Stanton De Priest'
},
feedback: "Text again.",
moreUrl: 'https://interactive.wttw.com/dusable-to-obama/carol-moseley-braun'
}, {
question: "What musical artist was part of Chicago's Black Renaissance?",
options: [{
option: "Louis Armstrong",
correct: false
}, {
option: "Nat \"King\" Cole",
correct: true
}, {
option: "Curtis Mayfield",
correct: false
}],
img: {
src: 'https://interactive.wttw.com/sites/default/files/dusable-to-obama_nat-king-cole.jpg',
alt: 'Nat King Cole'
},
feedback: "Long text.",
moreUrl: 'https://interactive.wttw.com/dusable-to-obama/dorseys-gospel'
}, {
question: "Gwendolyn Brooks was:",
options: [{
option: "the first black woman to win a Pulitzer Prize in poetry.",
correct: false
}, {
option: "all of the above",
correct: true
}],
img: {
src: 'https://interactive.wttw.com/sites/default/files/dusable-to-obama_gwendolyn-brooks.jpg',
alt: 'Gwendolyn Brooks'
},
feedback: "Gwendolyn Brooks (1917–2000) is a jewel in Chicago’s literary history. She was a writer best known for her poetry describing life in the South Side community in which she lived."
}]
}
}
updateAnswer(e) {
//record whether the question was answered correctly
let answerValue = e.target.value;
this.setState((prevState, props) => {
let stateToUpdate = prevState.questions;
//convert boolean string to boolean with JSON.parse()
stateToUpdate[prevState.currentQuestion].answerCorrect = JSON.parse(answerValue);
return {questions: stateToUpdate};
});
}
checkAnswer(e) {
//display to the user whether their answer is correct
this.setState((prevState, props) => {
let stateToUpdate = prevState.questions;
stateToUpdate[prevState.currentQuestion].checked = true;
return {questions: stateToUpdate};
});
}
nextQuestion(e) {
//advance to the next question
this.setState((prevState, props) => {
let stateToUpdate = prevState.currentQuestion;
return {currentQuestion: stateToUpdate+1};
}, () => {
this.radioRef.current.reset();
});
}
getResults() {
//loop through questions and calculate the number right
let correct = this.state.correct;
this.state.questions.forEach((item, index) => {
if (item.answerCorrect) {
++correct;
}
if (index === (this.state.questions.length-1)) {
this.setState({
correct: correct,
inProgress: false
});
}
});
}
startOver() {
//reset form and state back to its original value
this.setState((prevState, props) => {
let questionsToUpdate = prevState.questions;
questionsToUpdate.forEach(item => {
//clear answers from previous state
delete item.answerCorrect;
delete item.checked;
});
return {
inProgress: true,
correct: 0,
currentQuestion: 0,
questions: questionsToUpdate
}
});
}
componentDidMount() {
//since we're re-using the same form across questions,
//create a ref to it so we can clear its state after a question is answered
this.radioRef = React.createRef();
}
render() {
if (!this.state.inProgress) {
return (
<section className="quiz">
<Results correct={this.state.correct} questionLength={this.state.questions.length} startOver={this.startOver} />
</section>
);
}
return (
<section className="quiz fade-in" aria-live="polite">
<QuizProgress currentQuestion={this.state.currentQuestion} questionLength={this.state.questions.length} />
<div className="question-container">
{this.state.questions[this.state.currentQuestion].img.src &&
<QuestionImage img={this.state.questions[this.state.currentQuestion].img} />
}
<p className="question"><RawHTML html={this.state.questions[this.state.currentQuestion].question} /></p>
<form ref={this.radioRef}>
{this.state.questions[this.state.currentQuestion].options.map((item, index) => {
return <div key={index}
className={"option" + (this.state.questions[this.state.currentQuestion].checked && !item.correct ? ' dim' : '') + (this.state.questions[this.state.currentQuestion].checked && item.correct ? ' correct' : '')}>
<input id={"radio-"+index} onClick={this.updateAnswer} type="radio" name="option" value={item.correct}
disabled={this.state.questions[this.state.currentQuestion].checked} />
<label htmlFor={"radio-"+index}><RawHTML html={item.option}/></label>
</div>
})}
</form>
<div className="bottom">
{this.state.questions[this.state.currentQuestion].feedback && this.state.questions[this.state.currentQuestion].checked
&& <div className="fade-in">
<p>
<RawHTML html={this.state.questions[this.state.currentQuestion].feedback} />
{this.state.questions[this.state.currentQuestion].moreUrl &&
<React.Fragment>
<a target="_blank" href={this.state.questions[this.state.currentQuestion].moreUrl}>Learn more</a>.
</React.Fragment>
}
</p>
</div>
}
{!this.state.questions[this.state.currentQuestion].checked &&
<button type="button" onClick={this.checkAnswer}
disabled={!('answerCorrect' in this.state.questions[this.state.currentQuestion])}>Check answer</button>
}
{(this.state.currentQuestion+1) < this.state.questions.length && this.state.questions[this.state.currentQuestion].checked &&
<button className="fade-in next" type="button" onClick={this.nextQuestion}>Next <i className="fa fa-arrow-right"></i></button>
}
</div>
{(this.state.currentQuestion+1) === this.state.questions.length && this.state.questions[this.state.currentQuestion].checked &&
<button type="button" className="get-results pulse" onClick={this.getResults}>Get Results</button>
}
</div>
</section>
)
}
}
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<Quiz />, document.getElementById('riots-to-renaissance'));
})
and css
#import url('https://fonts.googleapis.com/css?family=Open+Sans');
#green: #36ad3b;
#red: #ff1100;
#yellow: #f3c000;
#blue: #1d77cc;
#sans: "Open Sans", "Helvetica", "Arial", sans-serif;
#keyframes roll-in {
0% {
top: 10px;
opacity: 0;
}
100% {
top: 0;
opacity: 1;
}
}
#keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
#keyframes pulse {
from {
transform: scale3d(1, 1, 1);
}
50% {
transform: scale3d(1.05, 1.05, 1.05);
}
to {
transform: scale3d(1, 1, 1);
}
}
.pulse {
animation: pulse 1s infinite;
}
.fade-in {
animation: fade .75s ease;
}
.quiz {
margin: 2em auto;
min-height: 40vh;
font-size: 16px;
.progress {
position: relative;
transition: width .4s ease;
margin-bottom: 1em;
background: rgb(181, 181, 181);
border-radius: 0;
width: 100%;
height: 2em;
font-family: #sans;
.progress-bar {
background-color: #1d77cc;
}
.counter {
position: absolute;
right: 5px;
top: 0;
font-weight: normal;
color: #fff;
height: 100%;
font-family: #sans;
font-size:1.25em;
margin: auto .5em;
letter-spacing:.025em;
display: flex;
flex-direction: column;
justify-content: center;
}
}
form {
width:90%;
margin:1.5em auto;
}
.img-fluid {
margin: 2em auto;
max-width: 360px;
display: block;
}
.question {
font-weight:bold;
line-height:1.35;
margin-bottom:.75em;
}
.option {
margin-bottom: .25em;
transition: all .25s ease;
font-size: .9em;
}
button {
padding: .75em;
font-family: #sans;
background-color: #1d77cc;
border: 0;
color: #fff;
font-size: 1em;
transition: .25s all;
white-space: nowrap;
font-weight: bold;
cursor: pointer;
i {
margin-left: .15em;
}
&:disabled {
opacity: .5;
}
}
//custom radio controls
input[type="radio"] {
position: absolute;
left: -9999px;
& + label {
position: relative;
font-weight: normal;
padding-left: 28px;
cursor: pointer;
line-height: 20px;
display: inline-block;
color: #666;
&::before {
text-align: center;
content: '';
position: absolute;
left: 0;
top: 0;
width: 20px;
height: 20px;
border: 1px solid #ddd;
border-radius: 100%;
background: #fff;
}
&::after {
content: '';
width: 12px;
height: 12px;
background-color: #222;
position: absolute;
top: 4px;
left: 4px;
border-radius: 100%;
transition: all 0.2s ease;
}
}
}
.dim, .correct {
input[type="radio"] + label::before {
border: 0;
font-size: 1.2em;
animation: .25s roll-in ease;
}
input[type="radio"] + label::after {
display: none;
}
}
.correct input[type="radio"] + label:before {
content: '\f00C';
font-family: "FontAwesome"!important;
color: #green;
}
.dim input[type="radio"]:checked + label:before {
content: '\f00d';
font-family: "FontAwesome"!important;
color: #red;
}
input[type="radio"]:not(:checked) + label:after {
opacity: 0;
transform: scale(0);
}
input[type="radio"]:checked + label:after {
opacity: 1;
transform: scale(1);
}
//end custom radio controls
.dim {
opacity: 0.5;
}
.bottom {
width:90%;
margin:0 auto;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
div {
flex: 1 1 70%;
font-size: .9em;
}
.next {
flex: 0 1 10%;
margin-left: 3em;
}
#media (max-width: 600px) {
div, .next {
flex-basis: 100%;
}
.next {
margin-left: 0;
}
}
}
.get-results {
display: block;
margin: 2em auto;
}
.results {
font-size: 1.1em;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 40vh;
h1 {
font-family: #sans;
}
button {
margin-top: 1em;
}
}
}
When I check my browser console, I get the error message:
Uncaught SyntaxError: Unexpected token '<'
I have question about Vue.js. How can i fix this? I didn't find anything in documentation. I've got this error: "[vue/require-v-for-key]
Elements in iteration expect to have 'v-bind:key' directives."
And this: "Elements in iteration expect to have 'v-bind:key' directives."
I have this i my Roulette.vue
<template>
<div class="roulette">
<h1>Roulette</h1>
<div class="radio" v-for="genre in genres"> **here**
<input
#change="onGenrePick"
type="radio"
name="genre"
v-bind:id="genre.id"
v-bind:value="genre.id">
<label v-bind:for="genre.id">{{genre.name}}</label>
</div>
<Button class="btn" :onClick="roll">Roll</Button>
<MovieCard
v-if="!!movie"
v-bind:image="serviceGetImagePath(movie.poster_path)"
v-bind:title="movie.title"
v-bind:releaseDate="serviceFormatYear(movie.release_date)"
v-bind:id="movie.id"
v-bind:voteAverage="movie.vote_average"/>
</div>
</template>
<script>
import MovieCard from '../components/MovieCard'
import Button from '../components/Button'
import {movieService} from '../mixins/movieService'
export default {
name: 'Roulette',
components: {Button, MovieCard},
mixins: [movieService],
mounted: async function () {
this.genres = await this.serviceGetGenres()
},
data: () => ({
genres: [],
pickedGenres: new Set(),
movie: null
}),
methods: {
async roll() {
const genreIds = Array.from(this.pickedGenres)
const movies = await this.serviceGetMoviesByGenre(genreIds)
this.movie = movies[this.getRandom(movies.length)]
},
onGenrePick(e) {
this.pickedGenres.add(e.target.value)
},
getRandom(max) {
return Math.floor(Math.random() * max - 1)
}
}
}
</script>
<style scoped lang="scss">
.roulette {
margin: 40px;
}
.btn {
display: block;
min-width: 220px;
}
.radio {
display: inline-block;
margin: 20px 10px;
> label {
margin-left: 5px;
}
}
</style>
And in my UpcomingMovies.vue also
<template>
<div class="wrapper" v-if="movies.length">
<MovieCard
v-for="movie in movies" **here**
v-bind:image="serviceGetImagePath(movie.poster_path)"**here**
v-bind:title="movie.title"**here**
v-bind:releaseDate="serviceFormatYear(movie.release_date)"**here**
v-bind:id="movie.id"**here**
v-bind:voteAverage="movie.vote_average"/>**here**
<div class="loader">
<Button class="loader__btn" :onClick="loadNextPage">Load</Button>
</div>
<router-link :to="routes.roulette.path">
<div class="roulette">
<img src="../assets/roulette.png" alt="Roulette">
</div>
</router-link>
</div>
<Loader v-else/>
</template>
<script>
import Button from '../components/Button'
import MovieCard from '../components/MovieCard'
import Loader from '../components/Loader'
import { movieService } from '../mixins/movieService'
import routes from '../router/routes'
export default {
name: 'UpcomingMovies',
mixins: [movieService],
components: { Button, MovieCard, Loader },
data: () => ({
movies: [],
page: 1,
routes
}),
mounted() {
this.getMovies(this.page)
},
methods: {
getMovies: async function (page) {
const movies = await this.serviceGetMovies(page)
this.movies.push(...movies)
},
loadNextPage() {
this.getMovies(++this.page)
}
}
}
</script>
<style scoped lang="scss">
.wrapper {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
margin-top: 40px;
margin-bottom: 40px;
}
.loader {
width: 100%;
text-align: center;
margin-top: 40px;
margin-bottom: 40px;
&__btn {
border: 5px dashed white;
background-color: transparent;
border-radius: 50%;
width: 80px;
height: 80px;
font-weight: bold;
text-transform: uppercase;
transition: border-radius 100ms ease-in-out, width 120ms ease-in-out 120ms;
&:hover {
border-radius: 0;
background-color: rgba(white, 0.1);
width: 200px;
}
}
}
.roulette {
cursor: pointer;
position: fixed;
right: 25px;
bottom: 25px;
> img {
opacity: .8;
animation: rotate 5s infinite;
width: 70px;
height: auto;
transition: opacity 220ms linear;
&:hover {
opacity: 1;
}
}
}
#keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>
Vue internally uses unique keys for each loop to determine which element to update at which point during an update process. Therefore every v-for needs a v-bind:key attribute to work properly. In your special case this would be as the following:
<div class="radio" v-for="genre in genres" v-bind:key="someUniqueId"> **here**
You can use the current loop index as ID or anything else.
I am trying to listen to a mouse event in a child component from the component, but I don't get the event being fired. It works when I listen for the event in html, but not
This works as you can see:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#mouse',
data: {
message: 'Hover Me!'
},
methods: {
mouseover: function() {
this.message = 'Good!'
},
mouseleave: function() {
this.message = 'Hover Me!'
}
}
});
body {
background: #333;
}
body #mouse {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: block;
width: 280px;
height: 50px;
margin: 0 auto;
line-height: 50px;
text-align: center;
color: #fff;
background: #007db9;
}
body #mouse a {
display: block;
width: 100%;
height: 100%;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="mouse">
<a #mouseover="mouseover" #mouseleave="mouseleave">
{{message}}
</a>
</div>
This one doesn't work because the event listening is done in code.
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#mouse',
data: {
message: 'Hover Me!'
},
methods: {
mouseover: function() {
this.message = 'Good!'
},
mouseleave: function() {
this.message = 'Hover Me!'
},
mounted() {
this.$on('mouseleave', this.mouseleave);
}
}
});
body {
background: #333;
}
body #mouse {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: block;
width: 280px;
height: 50px;
margin: 0 auto;
line-height: 50px;
text-align: center;
color: #fff;
background: #007db9;
}
body #mouse a {
display: block;
width: 100%;
height: 100%;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="mouse">
<a #mouseover="mouseover">
{{message}}
</a>
</div>
What is the correct way of manually listening for mouseleave event from the component itself rather than in the html?
In the second snippet the mounted hook function should be outside of methods, this will not solve the problem, and vm.$on function is used for custom event not for native ones like click and mouseleave, like explained here :
if you call this :
vm.$on('test', function (msg) {
console.log(msg)
})
You should have a code like the following one somewhere :
vm.$emit('test', 'hi')
Since you're not able to use this.$on method, i recommend you the following solution using ref by adding ref attribute to your a element by giving link or whatever you want and in the mounted hook add the following code:
this.$refs.link.addEventListener('mouseleave', () => {
this.mouseleave()
}, false);
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#mouse',
data: {
message: 'Hover Me!'
},
methods: {
mouseover: function() {
this.message = 'Good!'
},
mouseleave: function() {
this.message = 'Hover Me!'
}
},
mounted() {
this.$refs.link.addEventListener('mouseleave', () => {
this.mouseleave()
}, false);
}
});
body {
background: #333;
}
body #mouse {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: block;
width: 280px;
height: 50px;
margin: 0 auto;
line-height: 50px;
text-align: center;
color: #fff;
background: #007db9;
}
body #mouse a {
display: block;
width: 100%;
height: 100%;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="mouse">
<a #mouseover="mouseover" ref="link">
{{message}}
</a>
</div>
I built simple Modal component which will slide from bottom when opened. Animations are working fine when Modal trigger button clicked and backdrop clicked. But i am seeing slide-down animation at initial render of page. How can i prevent initial animation ?? I am specifically looking how to solve with react hooks.
Modal.js
import React, { useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import './Modal.css';
const Modal = ({ isOpen, onClose, children }) => {
const modalEl = useRef(null);
const handleCoverClick = (e) => {
if (e.target.hasAttribute('modal')) {
onClose();
}
}
useEffect(() => {
const handleAnimationEnd = (event) => {
if (!isOpen) {
event.target.classList.remove('show');
event.target.classList.add('hide');
} else {
event.target.classList.remove('hide');
event.target.classList.add('show');
}
};
modalEl.current.addEventListener('animationend', handleAnimationEnd);
return () => modalEl.current.removeEventListener('animationend', handleAnimationEnd);
}, [isOpen]);
return createPortal(
<>
<div className={`ModalCover ${isOpen ? 'show' : 'hide'}`} onClick={handleCoverClick} modal="true"></div>
<div className={`ModalContainer ${isOpen ? 'slide-up' : 'slide-down'}`} ref={modalEl}>
{children}
</div>
</>,
document.body);
};
export default Modal;
Modal.css
.show {
display: block;
}
.hide {
display: none;
}
.slide-up {
transform: translateY(0%);
animation: slide-up 0.5s forwards;
}
.slide-down {
transform: translateY(100%);
animation: slide-down 0.5s forwards;
}
#keyframes slide-up {
0% { transform: translateY(100%); }
100% { transform: translateY(0%); }
}
#keyframes slide-down {
0% { transform: translateY(0%); }
100% { transform: translateY(100%); }
}
.ModalCover {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background-color: rgba(0, 0, 0, 0.15);
}
.ModalContainer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 400px;
margin-top: calc(100vh - 400px);
z-index: 20;
}
demo (codesandbox) : https://codesandbox.io/s/l7x5p4k82m
Thanks!
A simpler way is to do this with classNames since direct DOM access is discouraged with DOM. modalEl.current ref is assigned after initial render, it can be used as a flag that a component was mounted:
<div className={`
ModalContainer
${isOpen ? 'slide-up' : 'slide-down'}
${!modalEl.current && 'hide'}
`} ref={modalEl}>
Applying hide class on component mount in useEffect may result in briefly shown modal animation.
In vue, I defined the component Message, the code is next:
<template>
<div class="j-message" v-if="show">
<Icon :type="newType"/>
<span class="message">
<slot></slot>
</span>
</div>
</template>
<style lang="less" rel="stylesheet/less">
#border: #e9e9e9;
#messagefontcolor: #666;
.j-message {
padding: 8px 16px;
border: 1px solid #border;
border-radius: 4px;
display: inline-block;
position: fixed;
left: 50%;
transform: translate3d(-50%, 0, 0);
top: 50px;
font-size: 12px;
color: #messagefontcolor;
box-shadow: 2px 2px 2px #border, -1px 0px 1px #border;
animation: messagedisplay .2s linear;
.message {
margin-left: 5px;
}
}
#keyframes messagedisplay {
0% {
opacity: 0;
transform: translate3d(-50%, -20px, 0);
}
100% {
opacity: 1;
transform: translate3d(-50%, 0, 0);
}
}
</style>
<script type="text/ecmascript-6">
import Icon from '../icon/icon.vue'
export default {
data() {
return {
show: true
}
},
computed: {
newType: function () {
return this.type ? this.type : "prompt"
},
time: function() {
if(this.duration) {
return this.duration * 1000;
}
return 1500;
}
},
components:{
"Icon": Icon
},
props: {
type: String,
},
mounted() {
console.log(123)
setTimeout(() => {
console.log(345)
this.show = false
}, this.time)
}
}
</script>
And I defined the component Icon like this:
<template>
<i class="jicon" :class="'jicon-' + type"></i>
</template>
<style lang="less" rel="stylesheet/less">
#default: #1f90e6;
#success: #89ce6d;
#fail: #fc561f;
#warning: #fda929;
.jicon {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
.jicon-forward {
float: right;
margin-top: 1px;
margin-left: 1px;
&:before {
content: "\e601";
}
}
.jicon-prompt {
color: #default;
&:before { content: "\e620"; }
}
.jicon-success {
color: #success;
&:before { content: "\e63a"; }
}
.jicon-fail {
color: #fail;
&:before { content: "\e613"; }
}
.jicon-warning {
color: #warning;
&:before { content: "\e6d4"; }
}
.jicon-search:before { content: "\e600"; }
.jicon-backward:before { content: "\e60a"; }
#font-face {
font-family: 'iconfont'; /* project id:"191439" */
src: url('//at.alicdn.com/t/font_t6inlill5jzl4n29.eot');
src: url('//at.alicdn.com/t/font_t6inlill5jzl4n29.eot?#iefix') format('embedded-opentype'),
url('//at.alicdn.com/t/font_t6inlill5jzl4n29.woff') format('woff'),
url('//at.alicdn.com/t/font_t6inlill5jzl4n29.ttf') format('truetype'),
url('//at.alicdn.com/t/font_t6inlill5jzl4n29.svg#iconfont') format('svg');
}
</style>
<script>
export default{
data(){
},
components:{},
props: {
type: String
}
}
</script>
But when the component is created, the show is set as false, but the console reported error
vue.min.js:6 Uncaught (in promise) TypeError: Cannot read property 'ob' of undefined at a.e.$destroy
What can be the error?
try using lowercase for the icon element:
<icon :type="newType"></icon>
EDIT
I put the below together and it works as expected:
const Icon = {
template: `
<div>
<i class="jicon" :class="'jicon-' + type"></i>
<div>type: {{ type }}</div>
</div>
`,
props: {
type: String
}
}
new Vue({
el: '#app',
data() {
return {
show: true
}
},
computed: {
newType: function() {
return this.type ? this.type : "prompt"
},
time: function() {
if (this.duration) {
return this.duration * 1000;
}
return 1500;
}
},
components: {
Icon
},
props: {
type: String,
},
mounted() {
console.log(123)
setTimeout(() => {
console.log(345)
this.show = false
}, this.time)
}
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<div class="j-message" v-if="show">
<icon :type="newType"></icon>
<span class="message">
<slot></slot>
</span>
<div>show: {{ show }}</div>
</div>
</div>
hope that helps ...