Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vue.js transition on changing selected element from a list

I have a list of Profiles that open an "edit profile" screen. This screen slided in from the left. When I select a profile, if there is a screen already selected, I want it to slide out first, change the selected profile data and then slide in.

What happens now is: when I first select one element, the screen slides in. When I change the selected element, screen stays and don't slide out and back in.

Here is a gif to show how it's behaving now:

enter image description here

My code is:

Vue Method:

editProfile: function (index){
    // this.editingProfile = false;
    this.setProfile(index);
    this.editingProfile = true;

}

Html View:

        <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%);
  }

How do I make it properly slide out and then back in when changing a profile?

like image 579
sigmaxf Avatar asked Aug 13 '18 17:08

sigmaxf


2 Answers

I think I was incorrect with my comment. One way you can do this is to leverage :key and a v-if so that you can tell Vue to render a panel if you have selected one and then transition between panels that way. You won't need to have a transition-group then.

The thing is :key is what tells Vue that everything has changed. If you leave it off, Vue tries to recycle as much as it can. See the docs: Transitioning Between Elements

When toggling between elements that have the same tag name, you must tell Vue that they are distinct elements by giving them unique key attributes. Otherwise, Vue’s compiler will only replace the content of the element for efficiency. Even when technically unnecessary though, it’s considered good practice to always key multiple items within a <transition> component.

Consider the minimal example below:

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>
like image 53
zero298 Avatar answered Oct 11 '22 01:10

zero298


The root cause is v-if="editingProfile" always true after showing one profile in your codes.

One solution is set it to false first, then in this.$nextTick to set it to true again. But you have to put this.editingProfile = true inside one setTimeout and delay time = transition time. Otherwise, slide out effect will be overwritten.

Like below demo:

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>

Or you can consider to use Group transition like below simple demo:

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>
like image 30
Sphinx Avatar answered Oct 11 '22 00:10

Sphinx