VueJS - Is there a way to conditionally apply a transition? - vue.js

What I would like to do is create an alert box component with a custom reveal transition that is optional, something like this (edited for brevity):
<template>
<transition v-if="withTransition">
<b-alert v-bind="this.$attrs" />
</transition>
</template>
I will have a withTransition prop that ideally will conditionally render only the transition effect, and not affect the rendering of the alert box.
v-if and v-show won't work because those properties would show and hide the alert component as well. Plus, AFAIK <transition> does not render as a DOM element, so I'm not sure how that could be conditionally rendered, if at all.
Any suggestions?

You can use dynamic transitions and bind to a transition name (using a computed property) that does nothing when you want the effect disabled.
For example...
new Vue({
el: '#app',
data: () => ({ withTransition: true, show: false }),
computed: {
computedTransition () {
return this.withTransition && 'fade'
}
}
})
.alert {
background-color: #f99;
border: 1px solid #f66;
padding: 1rem;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<div id="app">
<p>
<label>
With transition:
<input type="checkbox" v-model="withTransition"/>
</label>
</p>
<p><button type="button" #click="show = !show">Toggle alert</button></p>
<transition :name="computedTransition">
<p class="alert" v-if="show">Something happened</p>
</transition>
</div>

Use an inline ternary condition
If you just need to quickly check against a variable, this is an option:
<transition :name="someVariable == 'someValue' ? 'myTransitionA' : 'myTransitionB'">
someVariable could be anything (prop, data, computed, method...).
Set a transition name (e.g.'myTransitionB') to just an empty string '' if you wanted no transition at all for a particular condition.
I use this technique to toggle transitions based on different UI contexts (e.g. mobile vs desktop). For example, I want control bars to slide in-and-out in mobile, but I want them to be static on desktop.

Related

List Transitions work only for "enter" not for "leave"

Following the example in the docs, I'm using transition-group for a list of items. Strangely it works when items appear (enter), not when they disappear (leave), meaning they slide down in an animated fashion when appearing, but disappear instantly without animation: the leave animation failed. Why?
<template>
<div v-if="notifications.length">
<transition-group name="notifications">
<span
v-for="notification in notifications"
:key="notification.id"
>
<!-- content -->
</span>
</transition-group>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState({
notifications: state => state.notifications.notifications
})
}
}
</script>
<style lang="scss" scoped>
.notifications-enter-active,
.notifications-leave-active {
transition: all 0.5s;
}
.notifications-enter {
transform: translateY(-100%);
}
.notifications-leave-to {
opacity: 0;
}
</style>
Store
export const mutations = {
DELETE_NOTIFICATION (state, id) {
state.notifications.splice(
state.notifications.findIndex(item => item.id === id),
1
)
}
}
I couldn't reproduce the exact symptom with that code (demo 1), which only transitions on leave instead of enter in your scenario. The reason for that is because the span is display: inline, which prevents the transition.
The Vue docs provide a tip for this:
One important note is that these FLIP transitions do not work with elements set to display: inline. As an alternative, you can use display: inline-block or place elements in a flex context.
So, you can apply display: flex on the transition-group:
<template>
<transition-group class="container">
...
</transition-group>
</template>
<style>
.container {
display: flex;
}
</style>
demo 2
Or display: inline-block on the span to be transitioned:
<template>
<span class="notification-item">
...
</span>
</template>
<style>
.notification-item {
display: inline-block;
}
</style>
demo 3
Turns out by replacing <div v-if="notifications.length"> with <div v-if="notifications"> transitions now work. Even though this doesn't make any sense to me.
If anyone can explain in a comment that'd be nice :)

Slide transition on tab (one pushing the other)

I'm trying to achieve a slide transition between two tabs. One tab is supposed to come from the left pushing the other one to the right and the opposite for the other one.
The leave transition goes well but the tab just pop in straight away without starting where it is supposed to...
I have made a CodePen to reproduce what I've tried : Slide transition test on CodePen
Here is the HTML, it is just a div containting 2 buttons that change the visibility of two div that represents my tabs content.
<div id="transition-test" class="demo">
<div class="tabs">
<button v-for="tab in tabs" class="tab" :key="tab.id" #click="selectedTab = tab.id"> {{tab.text}}</button>
<transition name="slide-right">
<div v-show="1 === selectedTab" class="tab1" key="tab1"></div>
</transition>
<transition name="slide-left">
<div v-show="2 === selectedTab" class="tab2" key="tab2"></div>
</transition>
</div>
</div>
In order to do the transition I do have the following css :
.slide-left,
.slide-right{
position: absolute;
}
.slide-right-enter-to,
.slide-right-leave {
opacity: 1;
transform: translateX(0);
}
.slide-right-enter,
.slide-right-leave-to {
opacity: 0;
transform: translateX(100%);
}
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 500ms ease-in-out;
}
.slide-left-enter-to,
.slide-left-leave {
opacity: 1;
transform: translateX(0);
}
.slide-left-enter,
.slide-left-leave-to {
opacity: 0;
transform: translateX(-100%);
}
Does anyone have an idea about what I'm missing here ?
I found the issue. I don't know why in the Vue transition documentation the css class added at enter is v-enter but the class applied in reality is v-enter-from...
this css class :
.slide-left-enter
becomes :
.slide-left-enter-from
Instead of coding it by yourself, you can use npm version of the transition. It will also help you with its API, Guides and Examples, so that you don't have to worry about those.

How to design a reusable dialog box within children components in Vue?

I've been struggling with implementing a dialog box / modal design and behavior from inside of children components in Vue.
So here's the set up, I have a Vue component called "WorkersComponent". This component is just a list of workers assigned to some case fetched from the backend (Laravel). This component is reusable an can be in any place/case/ticket/lookup where a user would want to add workers to.
The component has an "add" button in it. Once clicked, I want a new component to appear at that location (at the click location), which could be a dropdown, modal, dialogue - doesn't really mater. This subcomponent has a search bar and some controls to fetch workers info and add them to the parent component.
My problem is that I can't figure out how to get the nesting / positioning to work. Because it is a child component, its position is always against the parent component, so I can only control it's position within that parent component, but I want it to be displaying on top of other DOM elements and components if necessary - whatever makes sense. Worst case scenario - I want it to be in the middle of the page at least.
Now how do I implement this? I probably want it to be a unique subcomponent, not a global generic modal. On top of it, if it were a global generic, then I have an idea of how to populate the modal with relevant options but how to pass them back to the component that called the modal - no idea. So I'm struggling with the approach. It seems like such a simple thing and yet, I can't find a viable solution.
<workers-component name="Assigned Workers">
<button <!-- Vue controls in here to invoke a modal/dialogue/dropdown --> >Add Worker</button>
<!-- The subcomponent itself -->
<workers-select-component />
</workers-component>
Here's an example from Gmail: wherever this search bar is (let's say it's a parent component), if I click on a triangle, it will expand this other pane, which will (1) appear wherever the search bar is and (2) cover other elements to display it and (3) not dismiss the pane until manually dismissed (which is easy but normal Bootstrap dropdowns don't support this).
Here's a solution:
Vue.component('ToggleDialog', {
props: ['state'],
template: `
<button
#click="$emit('toggle', state)"
class="dialog-button"
>
TOGGLE MODAL
</button>
`
})
Vue.component('DialogModal', {
props: ['state'],
template: `
<div
class="dialog-backdrop"
>
<div
class="dialog-button"
>
<toggle-dialog
:state="state"
#toggle="toggleModal"
/>
</div>
</div>
`,
methods: {
toggleModal(state) {
this.$emit('toggle', state)
}
}
})
new Vue({
el: "#app",
data() {
return {
isModalOpen: false
}
},
methods: {
toggleModal(state) {
this.isModalOpen = !state
}
}
})
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-button {
padding: 10px 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<toggle-dialog :state="isModalOpen" #toggle="toggleModal">
OPEN MODAL
</toggle-dialog>
<dialog-modal v-if="isModalOpen" :state="isModalOpen" #toggle="toggleModal" />
</div>
As you can see the modal is not the child of the button, but the child of the main app. toggle events are emitted (and the modal re-emits it) to the app that controls the state of the modal dialog.
For more complex apps it might not be the best. You could use an event bus (deprecated in Vue3) or Vuex (state management) to overcome this multiple emit-re-emit stuff.
EDIT: NEW SOLUTION
Vue.component('ToggleDialog', {
data() {
return {
isModalOpen: false
}
},
template: `
<div
class="toggle-modal-wrapper"
>
<button
#click="isModalOpen = !isModalOpen"
class="dialog-button"
>
TOGGLE MODAL
</button>
<dialog-modal
v-if="isModalOpen"
#toggle="isModalOpen = !isModalOpen"
>
<slot></slot>
</dialog-modal>
</div>
`
})
Vue.component('DialogModal', {
props: {
innerComponent: {
type: String
}
},
template: `
<div
class="dialog-backdrop"
>
<div>
<slot></slot>
<br />
<button
#click="$emit('toggle')"
class="dialog-button"
>
TOGGLE MODAL
</button>
</div>
</div>
`
})
new Vue({
el: "#app",
})
.toggle-modal-wrapper {
z-index: 10000;
}
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-button {
padding: 10px 15px;
}
.other-part {
z-index: 1000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<toggle-dialog>
<template>
This is the first.
</template>
</toggle-dialog>
<toggle-dialog>
<template>
This is the other.
</template>
</toggle-dialog>
<div class="other-part">
OTHER PART OF THE UI
</div>
</div>
You could try playing with slots if you want a reusable component - or even better: the render function.

How can I append a div inside a image container in vue dynamically?

I'm new to Vue, and I'm stuck here. I understand we can use $el to append div's to DOM dynamically as a child node. But how will I go about appending a div inside a image container dynamically. I'm using bootstrap vue.
<b-img
style= "position:relative"
:id="'og'+(i+1)"
:src="pageImage.pageValue"
class="page-image"
>
<div style="position:absolute; left:0; top: 0; height:100%; width:17%; border: 2px solid red;"/>
</b-img>
While using Vue direct DOM manipulations are not preferred (Add/Remove child elements) as changes performed like this will no longer be reactive.
Rather you can find a solution of rendering a div element conditionally using vue v-if directive.
You can add some code over here and let us know what exactly you want to achieve so that we can give you an appropriate solution.
Below should work
<b-img
style= "position:relative"
:id="'og'+(i+1)"
:src="pageImage.pageValue"
class="page-image"
>
</b-img>
<div style="position:absolute; left:0; top: 0; height:100%; width:17%; border: 2px solid red;"/>
For adding borders dynamically all over image you can check the fiddle here.
<div id="app">
<div style="position:relative;">
<b-img src="https://picsum.photos/300/150/?image=41" fluid-grow alt="Fluid-grow image" ref="bimg" #load="details"></b-img>
<div v-for="divborders in imgborders" :style="{left: `${(divborders-1)*20}px`, position:'absolute', top:' 0', height:'100%', width:'20px', border: '2px solid red'}"/>
</div>
</div>
new Vue({
el: '#app',
data: {
imgborders: 0,
},
mounted() {
this.$nextTick( () => {
this.details();
});
},
methods: {
details() {
this.imgborders = Math.floor(this.$refs.bimg.getBoundingClientRect().width/20);
}
}
})

jsplumb state machine on multiple tabs

I am designing a web page for state machine and I am using jsplumb state machine. I want to draw this state machine on multiple tabs. Tabs are created dynamically using 'New' button
<div id="tabs">
<ul>
<li>
Untitled-1
<span class="ui-icon ui-icon-close"></span>
</li>
</ul>
<div id="Untitled-1" class="content">
<div id="Untitled-1-content" class="fsm">
</div>
</div>
</div>
In the div area of class 'fsm', state machine is drawn dynamically by dragging state machine(SM) blocks and dropping it in 'fsm' div. If there are multiple tabs, after dropping SM block in fsm div, it is draggable in fsm div only on 1st tab and not in another tabs. For example, if there are 2 tabs, I can re-position SM blocks only in 1st tab, on 2nd tab when I start dragging SM block, it disappears from fsm div and its left-top coordinates become negative.
This is the code for fsm div -
$("div.fsm").droppable({
accept: ".cTemp, .rTemp",
hoverClass: "ui-state-hover",
drop: function (event, ui) {
var draggable = $(ui.draggable[0]);
if (draggable.attr("class").indexOf("rTemp") > -1) {
$("<div class='rBase' id='rect" + rectI + "'></div>").text("rect" + rectI).appendTo(this);
$("#rect" + rectI).append("<div class='ep'></div>").appendTo(this);
$("#rect" + rectI).addClass("draggable");
jsPlumb.draggable($(".rBase"),{containment: ".fsm", zIndex: 30});
} else if (draggable.attr("class").indexOf("cTemp") > -1) {
$("<div class='cBase' id='circle" + circleI + "'></div>").text("circle" + circleI).appendTo(this);
$("#circle" + circleI).append("<div class='ep'></div>").appendTo(this);
$("#circle" + circleI).addClass("draggable");
jsPlumb.draggable($(".cBase"),{containment: ".fsm", zIndex: 30});
}
jsPlumbDemo.init();
}
});
This might be because fsm div is created dynamically or there are more than 1 fsm divs. What would be the best option to handle this multitab situation ?
I tried to remove fsm div from all tabs except the active tab then it works. I can drag SM blocks from tab content even if there are more than 1 tabs since there is only 1 fsm div. But then I have to add fsm div back again to the tab when I switch to that tab.
Then which is the best way to save the tab's content before switching to any other tab and load it back when that tab is opened ?
To give my background, this is my first time to work on jquery/jsplumb so detailed explanation is very much appreciated.
Here is a couple of comments
You can use the Jquery command hasClass() instead of attr("class")indexOf
To handle tabs I guess you are using the Jquery command tabs() ? in that case you don't need to save anything.
As far as your dropping issue is concerned you should use the appendTo: property in the draggable() parameters specifying which div they are supposed to be dropped to. You can change the target tab by reacting to the jquery tab select event this way
$("#tabs").tabs({
select: function(event, ui) {
// Your code
}
});
You can play with this fiddle I made for you
http://jsfiddle.net/webaba/sy9PJ/
$(document).ready(function () {
// Calls custom select tab function, scroll down for implementation
selectTab(1);
// Block models are made draggable
function selectTab(tabIndex) {
var tabName = '#tab'+tabIndex;
$(".model").draggable({
appendTo: tabName,
helper: 'clone'
});
}
// Tabs div is formatted as tabs by jquery
$("#tabs").tabs({
select: function (event, ui) {
selectTab(ui.index + 1);
}
});
//Sets canvas as droppable
$(".tab").droppable({
accept: ".model",
drop: function (event, ui) {
var newBlock = ui.helper.clone();
$(newBlock).removeClass("model").addClass("block");
$(this).append($(newBlock));
$(newBlock).draggable({containment:$(this)});
return true;
}
});
});
and the html
<body>
<div id="container">
<div id="library">
<div class="model type1" type="1">Block1</div>
<div class="model type2" type="2">Block2</div>
</div>
<div id="tabs">
<ul>
<li>Tab1
</li>
<li>Tab2
</li>
<li>Tab3
</li>
</ul>
<div id="tab1" class="tab">Tab1</div>
<div id="tab2" class="tab">Tab2</div>
<div id="tab3" class="tab">Tab3</div>
</div>
</div>
</body>
and the CSS
#container {
width:100%;
height:300px;
}
#tabs {
height:300px;
width:70%;
float:right;
border:1px solid black;
}
.tab{
height:100%;
border:1px dotted blue;
}
#library {
border:1px solid black;
width:20%;
height:300px;
float:left;
}
.type1 {
width:50px;
height:50px;
border:1px solid green;
}
.type2 {
width:50px;
height:20px;
border:1px solid red;
}