Search code examples
javascriptvue.jstransitionorder-of-execution

Vue.js Transitions and effects executing out of order


I'm making a Tic-Tac-Toe web app. It isn't finished yet as this problem has thrown up a road block. When either X or O wins, it should turn the 3 winning boxes green (this part works if I remove the transition that I want to happen after that). Once the boxes are green, the entire 'Board' table should 'bounce out'. But what's actually happening is that the 'Board' is bouncing out before the 'mark' (X/O) is even shown and the winning boxes turned green. What's not making sense to me is that the focus of the code execution should be from top to bottom but it doesn't appear to be behaving like that. I've tried rearranging the code several different ways and still no luck.

Here is the Codepen

And here is the code for those who don't want to click the link :P (Sorry for the styling, the code snippet doesn't want to work correctly =/

BTW, thanks in advance!

console.clear();
const log = console.log.bind(console);

const game = new Vue({
	el: '#app',
	data: {
		turn: 'X',
		over: false,
		board: [[{val:'',bg:''}, {val:'',bg:''}, {val:'',bg:''}],
					  [{val:'',bg:''}, {val:'',bg:''}, {val:'',bg:''}],
					  [{val:'',bg:''}, {val:'',bg:''}, {val:'',bg:''}]],
		windex: [[[0,0], [0,1], [0,2]],
						 [[1,0], [1,1], [1,2]],
						 [[2,0], [2,1], [2,2]],
						 [[0,0], [1,0], [2,0]],
						 [[0,1], [1,1], [2,1]],
						 [[0,2], [1,2], [2,2]],
						 [[0,0], [1,1], [2,2]],
						 [[0,2], [1,1], [2,0]]],
		check() {
			const arr = this.board.map( x => x.map( y => y.val ));
			const winArr = this.windex.map( x => x.map( y => this.board[y[0]][y[1]].val ));
			const winner = winArr.map( (x,ind) => {
				if( x.every( y => y == 'X' )) return 'X';
				if( x.every( y => y == 'O' )) return 'O';
			});
			if(winner.includes('X')){
				const inds = this.windex[winner.indexOf('X')];
				inds.forEach( x => {
					this.board[x[0]][x[1]].bg = 'active';
				});
				this.over = true;
			};
			if(winner.includes('O')){
				const inds = this.windex[winner.indexOf('O')];
				inds.forEach( x => {
					this.board[x[0]][x[1]].bg = 'active';
				});
				this.over = true;
			};
			if(arr.every( x => x.every( y => y == 'X' || y == 'O' )))
				this.over = true;
		}
	},
	methods: {
		mark(box) {
			if(this.over) return
			if(box.val === ''){
				box.val = this.turn;
				this.turn = this.turn == 'X' ? 'O' : 'X';
			} else 
					alert('Invalid turn')
				this.check()
		}
	}
});
@import 'https://fonts.googleapis.com/css?family=Oswald';

h1 {
	font-family: 'Oswald';
	letter-spacing: 1.5vw;
	text-align: center;
	margin:1vw;
}

table {
	margin-left: auto;
	margin-right: auto;
	border-collapse: separate;
	border-spacing: 2px;
}

.square {
	width: 100px;
	height: 100px;
	background-color: #6C7A89;
	text-align: center;
	color: white;
	cursor: pointer;
	text-align: center;
	line-height: 100px;
	font-size: 50px;
	font-family: 'Oswald';
	display: block;
}

.square:hover {
	opacity: .8;
}

td {
	vertical-align: middle;
}

.active {
	background-color: #00B16A;
}

.bounce-leave-active {
  animation: bounce-out 1.5s;
}

@keyframes bounce-out {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(0);
  }
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js"></script>

<div id='app'>
	<h1>TIC-TAC-TOE</h1>
	<transition name='bounce'>
	<table v-if='!over'>
		<tr v-for='row in board'>
			<td v-for='box in row'>
				<div class='square' 
						 v-bind:class='{active:box.bg}' 
						 v-on:click='mark(box)'>
					{{box.val}}
				</div>
			</td>
		</tr>
	</table>
	</transition>
</div>


Solution

  • Your code runs as a block; the UI doesn't repaint between your setting active and over, so they happen effectively at the same time as far as the UI is concerned. The over triggers the v-if binding, so the contents don't get repainted, they just transition out.

    Vue provides nextTick to allow you to sequence things. Like setTimeout(..., 0), it takes your command out of the block, but it ensures that a DOM update cycle has happened before executing.