javascript - Transición en Vue.js al cambiar el elemento seleccionado de una lista
Tengo una lista de Perfiles que abre una pantalla de "editar perfil". Esta pantalla se deslizó desde la izquierda. Cuando selecciono un perfil, si ya hay una pantalla seleccionada, quiero que primero se deslice hacia fuera, cambie los datos del perfil seleccionado y luego se deslice hacia dentro.
Lo que sucede ahora es: cuando primero selecciono un elemento, la pantalla se desliza. Cuando cambio el elemento seleccionado, la pantalla permanece y no se desliza hacia afuera y hacia adentro.
Aquí hay un gif para mostrar cómo se está comportando ahora:
Mi código es:
Método de Vue:
editProfile: function (index){
// this.editingProfile = false;
this.setProfile(index);
this.editingProfile = true;
}
Vista HTML:
<transition name="fade" mode="out-in">
<div v-if="editingProfile" id="edit-profile">
<input placeholder="Profile Name" v-model="synced.profiles[synced.selectedProfile].name">
</div>
</transition>
CSS :
.fade-enter-active, .fade-leave-active {
transition: all .2s;
/* transform:translateX(0); */
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
transform:translateX(-100%);
}
¿Cómo hago que se deslice correctamente hacia afuera y luego vuelva a entrar al cambiar un perfil?
Respuesta #1
Creo que estaba equivocado con mi comentario. Una forma en que puedes hacer esto es aprovechar :key y un v-if para que puedas indicarle a Vue que renderice un panel si has seleccionado uno y luego hacer la transición entre los paneles de esa manera. Entonces no necesitarás tener un transition-group.
La cosa es que :key es lo que le dice a Vue que todo ha cambiado. Si lo omites, Vue intenta reciclar tanto como puede. Consulta la documentación: Transición entre elementos
Al alternar entre elementos que tienen el mismo nombre de etiqueta, debes indicarle a Vue que son elementos distintos dándoles atributos
keyúnicos. De lo contrario, el compilador de Vue solo reemplazará el contenido del elemento por eficiencia. Aunque técnicamente no sea necesario, se considera una buena práctica siempre asignar una key a múltiples ítems dentro de un componente<transition>.
Considera el ejemplo mínimo a continuación:
const panels = [{
title: "Hello"
},
{
title: "World"
},
{
title: "Foo"
},
{
title: "Bar"
}
];
const app = new Vue({
el: "#app",
data() {
return {
panels,
activePanel: null
};
},
computed: {
titles() {
return this.panels.map(panel => panel.title);
}
},
methods: {
handleTitleClick(idx) {
if (this.activePanel === idx) {
this.activePanel = null;
return;
}
this.activePanel = idx;
}
}
});body {
margin: 0;
padding: 0;
}
#app {
display: flex;
align-items: stretch;
height: 100vh;
}
#panel-set {
flex: 1 0 70%;
display: flex;
}
#side-panel {
flex: 1 0 30%;
}
.panel {
padding: 1em;
flex: 1 0;
background-color: rgba(0, 0, 0, 0.2);
}
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: transform 500ms ease-in-out;
}
.slide-fade-enter,
.slide-fade-leave-to {
transform: translateX(-100%);
}<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<div id="app">
<div id="panel-set">
<transition name="slide-fade" mode="out-in">
<div class="panel" v-if="activePanel !== null" :key="activePanel">
<h2>{{panels[activePanel].title}}</h2>
<p>Lorem ipsum</p>
</div>
</transition>
</div>
<div id="side-panel">
<ul>
<li v-for="(title, idx) in titles" @click="handleTitleClick(idx)">{{title}}</li>
</ul>
</div>
</div>Respuesta #2
La causa raíz es v-if="editingProfile" siempre verdadero después de mostrar un perfil en tu código.
Una solución es configurarlo en falso primero, luego en this.$nextTick volver a configurarlo en verdadero. Pero tienes que poner this.editingProfile=true dentro de un setTimeout y el tiempo de retraso=tiempo de transición. De lo contrario, el efecto de slide out se sobrescribirá.
Como en la demostración a continuación:
new Vue({
el: '#emit-example-simple',
data() {
return {
editingProfile: false,
synced : {
profiles: [{'name':'A'}, {'name':'B'}, {'name':'C'}],
selectedProfile: 0
},
}
},
methods: {
editProfile: function (index){
this.editingProfile = !this.editingProfile
this.$nextTick(() => {
setTimeout(()=> {
this.synced.selectedProfile = index
this.editingProfile = true
}, 1200)
})
}
}
}).fade-enter-active, .fade-leave-active {
transition: all 1.2s;
/* transform:translateX(0); */
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
transform:translateX(-100%);
border: 1px solid white;
}<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="emit-example-simple">
<button @click="editProfile(0)">Profile 1</button>
<button @click="editProfile(1)">Profile 2</button>
<button @click="editProfile(2)">Profile 3</button>
<transition name="fade" mode="out-in">
<div v-if="editingProfile" id="edit-profile">
<input style="border: 5px solid red;" placeholder="Profile Name" v-model="synced.profiles[synced.selectedProfile].name">
</div>
</transition>
</div>O puedes considerar usar Transición de grupo como el siguiente demo sencillo:
new Vue({
el: '#emit-example-simple',
data() {
return {
editingProfile: false,
profileContainers: [true, false],
synced : {
profiles: [{'name':'A'}, {'name':'B'}, {'name':'C'}],
selectedProfile: 0
},
}
},
methods: {
editProfile: function (index){
this.synced.selectedProfile = index
this.profileContainers = this.profileContainers.map((x)=>!x)
}
}
}).list-items-enter-active {
transition: all 1.2s;
}
.list-items-leave-active {
transition: all 1.2s;
}
.list-items-enter, .list-items-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
transform:translateX(-100%);
border: 1px solid white;
}
.list-item {
display: inline-block;
border: 6px solid red;
}<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="emit-example-simple">
<button @click="editProfile(0)">Profile 1</button>
<button @click="editProfile(1)">Profile 2</button>
<button @click="editProfile(2)">Profile 3</button>
<transition-group name="list-items" tag="p">
<div v-for="(item, index) in profileContainers" :key="index" v-if="item">
<input style="border: 5px solid red;" placeholder="Profile Name" v-model="synced.profiles[synced.selectedProfile].name">
</div>
</transition-group>
</div>