Reputation: 9701
const NotificationBar = {
name: 'notification-bar',
template: `
<div
:class="{
'notification-bar': true,
'notification-bar--error': isError,
'notification-bar--warning': isWarning,
'notification-bar--info': isInfo,
'notification-bar--visible': isVisible,
}"
@click="dismiss"
@transitionend="transitionEnd($event)">
{{ message }}
</div>
`,
props: {
message: {
type: String,
required: true,
},
type: {
type: String,
required: true,
validator(value) {
const valid = ['error', 'warning', 'info'];
return valid.includes(value);
},
},
dismissable: {
type: Boolean,
default: false,
},
timeout: {
type: Number,
default: 0,
},
},
data() {
return {
isVisible: false,
};
},
computed: {
isError() {
return this.type === 'error';
},
isWarning() {
return this.type === 'warning';
},
isInfo() {
return this.type === 'info';
},
},
methods: {
clear() {
const event = 'cleared';
let done;
if (this.isVisible) {
this.$once('transitionend', () => {
done = true;
this.$emit(event, done);
});
this.isVisible = false;
} else {
done = false;
this.$emit(event, done);
}
},
dismiss() {
const event = 'dismissed';
let done;
if (this.dismissable) {
done = true;
this.$emit(event, done);
this.clear();
} else {
done = false;
this.$emit(event, done);
}
},
show() {
if (!this.isVisible) {
this.isVisible = true;
this.$emit('show', this.clear);
if (this.timeout) {
setTimeout(() => {
this.$emit('timeout');
this.clear();
}, this.timeout);
}
}
},
transitionEnd(event) {
this.$emit('transitionend', event);
},
},
mounted() {
window.requestAnimationFrame(this.show);
},
};
const NotificationCenter = {
name: 'notification-center',
components: {
NotificationBar,
},
template: `
<div>
<notification-bar
v-for="notification in active"
:message="notification.message"
:type="notification.type"
:dismissable="notification.dismissable"
:timeout="notification.timeout"
@cleared="clear">
</notification-bar>
</div>
`,
props: {
queue: {
type: Array,
required: true,
},
},
data() {
return {
active: [],
};
},
computed: {
hasActiveNotification() {
return this.active.length > 0;
},
hasQueuedNotification() {
return this.queue.length > 0;
},
},
watch: {
queue() {
if (this.hasQueuedNotification && !this.hasActiveNotification) {
this.setNextActive();
}
},
},
methods: {
setNextActive() {
this.setActive(this.queue.shift());
},
setActive(notification) {
this.active.push(notification);
},
removeActive() {
this.active.pop();
},
clear() {
this.active.pop();
if (this.hasQueuedNotification) {
this.$nextTick(this.setNextActive);
}
},
},
};
window.vm = new Vue({
components: {
NotificationCenter,
},
el: '#app',
template: `
<div>
<notification-center
:queue="notifications">
</notification-center>
<label>
<strong>Type</strong> <br>
Error <input v-model="type" type="radio" name="type" value="error"> <br>
Warning <input v-model="type" type="radio" name="type" value="warning"> <br>
Info <input v-model="type" type="radio" name="type" value="info"> <br>
</label>
<label>
<strong>Message</strong>
<input v-model="message" type="text">
</label>
<label>
<strong>Dismissable</strong>
<input v-model="dismissable" type="checkbox">
</label>
<label>
<strong>Timeout</strong>
<input v-model="timeout" type="number" step="100" min="0">
</label>
<button @click="generateNotification">Generate notification</button>
</div>
`,
data: {
notifications: [],
type: null,
message: null,
dismissable: null,
timeout: null,
},
methods: {
generateNotification() {
const {
type,
message,
dismissable,
timeout,
} = this;
this.notifications.push({
type,
message,
dismissable,
timeout,
});
this.type = this.message = this.dismissable = this.timeout = null;
},
},
});
.notification-bar {
box-sizing: border-box;
position: absolute;
top: -3.2rem;
right: 0;
left: 0;
z-index: 9999;
width: 100%;
height: 3.2rem;
color: #fff;
font-family: 'Avenir Next', sans-serif;
font-size: 1.2em;
line-height: 3.2rem;
text-align: center;
transition: top 266ms ease;
}
.notification-bar--error {
background-color: #f02a4d;
}
.notification-bar--warning {
background-color: #ffc107;
}
.notification-bar--info {
background-color: #2196f3;
}
.notification-bar--visible {
top: 0;
}
label {
display: block;
margin: 2rem 0;
font-size: 1.4rem;
}
label:first-of-type {
margin-top: 5rem;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
html {
font-size: 62.5%;
}
body {
font-family: sans-serif;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
</body>
</html>
Notification bar smoothly animates into the visible viewport. You can observe this by doing the steps above in Firefox.
Here is a GIF demonstrating the correct behaviour in Chrome. Upon clicking Generate notification you can see the bar smoothly transitioning in.
Here is the screenshot of the timeline when Chrome is behaving correctly:
Here is the call tree when Chrome is behaving correctly:
Notification bar does not smoothly animate into the visible viewport the majority of the time. Capturing a timeline in Chrome Devtools shows no animation running when notification bar is being shown. Animations always run correctly when bar is animating off screen. Animation always runs correctly in Firefox.
Here is a GIF demonstrating the incorrect behaviour in Chrome. Upon clicking Generate notification you can see the bar suddenly appearing.
Here is the screenshot of the timeline when Chrome is not behaving correctly:
Here is the call tree when Chrome is not behaving correct:
Outline of what code is doing:
NotificationCenter
takes in a queue
prop. This is an array of objects with the array representing a queue of notifications and an object representing a single notification.
Once the queue
changes, a watcher runs checking if there are notifications in the queue and if there isn't an active notification. If this is the case, the next notification is set as the active notification.
NotificationCenter
's template has a directive looping over items in active
and renders a NotificationBar
. In the previous step, a new active notification was set thus a new notification bar will be created and mounted to the DOM.
Once the NotificationBar
is mounted on the DOM, its show
method is run inside of window.requestAnimationFrame
.
Upvotes: 1
Views: 4506
Reputation: 9701
After some discussion with LinusBorg, a contributor to Vue, on the Vue forum we had a possible cause for this problem:
[...] the issue is likely that Vue patches the DOM asynchronously, so when
mounted()
is called, the elements of the components exist, but they are not guranteed to be in the DOM.And so now, depending on how different browsers handle the priorities of normal tasks, microtasks and animationFrames, it may simply be the case that in Chrome, the element is not in the DOM yet, when you change the class through
show()
In that case, the animation effect would not appear, naturally.
I suggest to try
this.$nextTick()
instead (which guarantees that the element is already in the DOM), or simply use the tools Vue gives you for this, namely the<transition>
component.
– LinusBorg, https://forum.vuejs.org/t/what-is-causing-this-broken-animation-transition-in-a-vue-js-component-in-chrome/7742/7
Attempts to use this.$nextTick
were made initially but failed in both Firefox and Chrome.
Eventually I was able to implement this whole thing using the <transition>
component.
const NotificationBar = {
name: 'notification-bar',
template: `
<transition
name="visible"
mode="out-in"
@after-enter="show">
<div
:class="{
'notification-bar': true,
'notification-bar--error': isError,
'notification-bar--warning': isWarning,
'notification-bar--info': isInfo,
'notification-bar--visible': isVisible,
}"
:key="id"
@click="dismiss">
{{ message }}
</div>
</transition>
`,
props: {
message: {
type: String,
required: true,
},
type: {
type: String,
required: true,
validator(value) {
const valid = ['error', 'warning', 'info'];
return valid.includes(value);
},
},
id: {
type: [Number, String],
required: true,
},
dismissable: {
type: Boolean,
default: false,
},
timeout: {
type: Number,
default: 0,
},
},
data() {
return {
isVisible: false,
};
},
computed: {
isError() {
return this.type === 'error';
},
isWarning() {
return this.type === 'warning';
},
isInfo() {
return this.type === 'info';
},
},
methods: {
clear() {
const event = 'clear';
let done;
if (this.isVisible) {
done = true;
this.$emit(event, done);
this.isVisible = false;
} else {
done = false;
this.$emit(event, done);
}
},
dismiss() {
const event = 'dismissed';
let done;
if (this.dismissable) {
done = true;
this.$emit(event, done);
this.clear();
} else {
done = false;
this.$emit(event, done);
}
},
show() {
if (!this.isVisible) {
this.isVisible = true;
this.$emit('show', this.clear);
if (this.timeout) {
setTimeout(() => {
this.$emit('timeout');
this.clear();
}, this.timeout);
}
}
},
},
};
const NotificationCenter = {
name: 'notification-center',
template: `
<div>
<notification-bar
v-if="hasQueuedNotification"
:message="activeNotification.message"
:type="activeNotification.type"
:dismissable="activeNotification.dismissable"
:timeout="activeNotification.timeout"
:id="activeNotification.id"
@clear="clear">
</notification-bar>
</div>
`,
components: {
NotificationBar,
},
props: {
queue: {
type: Array,
required: true,
},
},
computed: {
hasQueuedNotification() {
return this.queue.length > 0;
},
activeNotification() {
return this.queue[0];
},
},
methods: {
clear() {
this.queue.shift();
},
},
};
window.vm = new Vue({
components: {
NotificationCenter,
},
el: '#app',
template: `
<div>
<notification-center
:queue="notifications">
</notification-center>
<label>
<strong>Type</strong> <br>
Error <input v-model="type" type="radio" name="type" value="error"> <br>
Warning <input v-model="type" type="radio" name="type" value="warning"> <br>
Info <input v-model="type" type="radio" name="type" value="info"> <br>
</label>
<label>
<strong>Message</strong>
<input v-model="message" type="text">
</label>
<label>
<strong>Dismissable</strong>
<input v-model="dismissable" type="checkbox">
</label>
<label>
<strong>Timeout</strong>
<input v-model="timeout" type="number" step="100" min="0">
</label>
<button @click="generateNotification">Generate notification</button>
</div>
`,
data: {
notifications: [],
type: null,
message: null,
dismissable: null,
timeout: null,
dismissIndex: null,
dismissMessage: null,
},
methods: {
generateNotification() {
const {
type,
message,
dismissable,
timeout,
} = this;
const id = Date.now();
this.notifications.push({
type,
message,
dismissable,
timeout,
id,
});
this.type = this.message = this.dismissable = this.timeout = null;
},
},
});
.notification-bar {
box-sizing: border-box;
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 9999;
width: 100%;
height: 3.2rem;
color: #fff;
font-family: 'Avenir Next', sans-serif;
font-size: 1.2em;
line-height: 3.2rem;
text-align: center;
}
.notification-bar--error {
background-color: #f02a4d;
}
.notification-bar--warning {
background-color: #ffc107;
}
.notification-bar--info {
background-color: #2196f3;
}
.notification-bar.visible-enter, .notification-bar.visible-leave-to {
top: -3.2rem;
}
.notification-bar.visible-enter-to, .notification-bar.visible-leave {
top: 0;
}
.notification-bar.visible-enter-active, .notification-bar.visible-leave-active {
transition: top 266ms ease;
}
/* ================================================================== */
/* */
/* ================================================================== */
html {
font-size: 62.5%;
}
body {
margin: 0;
border: 1px solid black;
font-family: sans-serif;
}
label {
display: block;
margin: 2rem 0;
font-size: 1.4rem;
}
label:first-of-type {
margin-top: 5rem;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title></title>
</head>
<body>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
</body>
</html>
Upvotes: 1
Reputation: 73609
The issue is with this line:
this.type = this.message = this.dismissable = this.timeout = null;
If you remove this it works fine. as when this gets executed props becomes NULL and you have validation that props should not be null.
const NotificationBar = {
name: 'notification-bar',
template: `
<div
:class="{
'notification-bar': true,
'notification-bar--error': isError,
'notification-bar--warning': isWarning,
'notification-bar--info': isInfo,
'notification-bar--visible': isVisible,
}"
@click="dismiss"
@transitionend="transitionEnd($event)">
{{ message }}
</div>
`,
props: {
message: {
type: String,
required: true,
},
type: {
type: String,
required: true,
validator(value) {
const valid = ['error', 'warning', 'info'];
return valid.includes(value);
},
},
dismissable: {
type: Boolean,
default: false,
},
timeout: {
type: Number,
default: 0,
},
},
data() {
return {
isVisible: false,
};
},
computed: {
isError() {
return this.type === 'error';
},
isWarning() {
return this.type === 'warning';
},
isInfo() {
return this.type === 'info';
},
},
methods: {
clear() {
const event = 'cleared';
let done;
if (this.isVisible) {
this.$once('transitionend', () => {
done = true;
this.$emit(event, done);
});
this.isVisible = false;
} else {
done = false;
this.$emit(event, done);
}
},
dismiss() {
const event = 'dismissed';
let done;
if (this.dismissable) {
done = true;
this.$emit(event, done);
this.clear();
} else {
done = false;
this.$emit(event, done);
}
},
show() {
if (!this.isVisible) {
this.isVisible = true;
this.$emit('show', this.clear);
if (this.timeout) {
setTimeout(() => {
this.$emit('timeout');
this.clear();
}, this.timeout);
}
}
},
transitionEnd(event) {
this.$emit('transitionend', event);
},
},
mounted() {
window.requestAnimationFrame(this.show);
},
};
const NotificationCenter = {
name: 'notification-center',
components: {
NotificationBar,
},
template: `
<div>
<notification-bar
v-for="notification in active"
:message="notification.message"
:type="notification.type"
:dismissable="notification.dismissable"
:timeout="notification.timeout"
@cleared="clear">
</notification-bar>
</div>
`,
props: {
queue: {
type: Array,
required: true,
},
},
data() {
return {
active: [],
};
},
computed: {
hasActiveNotification() {
return this.active.length > 0;
},
hasQueuedNotification() {
return this.queue.length > 0;
},
},
watch: {
queue() {
if (this.hasQueuedNotification && !this.hasActiveNotification) {
this.setNextActive();
}
},
},
methods: {
setNextActive() {
this.setActive(this.queue.shift());
},
setActive(notification) {
this.active.push(notification);
},
removeActive() {
this.active.pop();
},
clear() {
this.active.pop();
if (this.hasQueuedNotification) {
this.$nextTick(this.setNextActive);
}
},
},
};
window.vm = new Vue({
components: {
NotificationCenter,
},
el: '#app',
template: `
<div>
<notification-center
:queue="notifications">
</notification-center>
<label>
<strong>Type</strong> <br>
Error <input v-model="type" type="radio" name="type" value="error"> <br>
Warning <input v-model="type" type="radio" name="type" value="warning"> <br>
Info <input v-model="type" type="radio" name="type" value="info"> <br>
</label>
<label>
<strong>Message</strong>
<input v-model="message" type="text">
</label>
<label>
<strong>Dismissable</strong>
<input v-model="dismissable" type="checkbox">
</label>
<label>
<strong>Timeout</strong>
<input v-model="timeout" type="number" step="100" min="0">
</label>
<button @click="generateNotification">Generate notification</button>
</div>
`,
data: {
notifications: [],
type: null,
message: null,
dismissable: null,
timeout: null,
},
methods: {
generateNotification() {
const {
type,
message,
dismissable,
timeout,
} = this;
this.notifications.push({
type,
message,
dismissable,
timeout,
});
//this.type = this.message = this.dismissable = this.timeout = null;
},
},
});
.notification-bar {
box-sizing: border-box;
position: absolute;
top: -3.2rem;
right: 0;
left: 0;
z-index: 9999;
width: 100%;
height: 3.2rem;
color: #fff;
font-family: 'Avenir Next', sans-serif;
font-size: 1.2em;
line-height: 3.2rem;
text-align: center;
transition: top 266ms ease;
}
.notification-bar--error {
background-color: #f02a4d;
}
.notification-bar--warning {
background-color: #ffc107;
}
.notification-bar--info {
background-color: #2196f3;
}
.notification-bar--visible {
top: 0;
}
label {
display: block;
margin: 2rem 0;
font-size: 1.4rem;
}
label:first-of-type {
margin-top: 5rem;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
html {
font-size: 62.5%;
}
body {
font-family: sans-serif;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
</body>
</html>
You have to put some validation in setActive
function, as if you push empty item in active, some validations you have are failing.
setActive(notification) {
if(notification.message){
this.active.push(notification);
}
},
check this fiddle.
Upvotes: 0