Using vue transition-groups, is there a way to trigger the leave + enter transitions instead of the move transitions for moving elements?
It should leave, and enter at the new position instead. The move transition only seems to work with transformations.
Playground: https://codepen.io/anon/pen/WqJEmV
HTML:
<div id="flip-list-demo" class="demo">
<button v-on:click="shuffle">Shuffle</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" v-bind:key="item">
{{ item }}
</li>
</transition-group>
</div>
JS:
new Vue({
el: '#flip-list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9]
},
methods: {
shuffle: function () {
this.items = _.shuffle(this.items)
}
}
})
CSS:
/** Should NOT use this: **/
.flip-list-move {
transition: transform 1s;
}
/** Should use this instead: **/
.flip-list-enter-active, .flip-list-leave-active {
transition: all 1s;
}
.flip-list-enter {
opacity: 0;
transform: translateX(80px);
}
.flip-list-leave-to {
opacity: 0;
transform: translateY(30px);
}
I had a similar issue so I thought I'd post here for anyone else who finds this later:
The solution that worked for me is to utilize the key attribute to force vue to treat the item that moved as a "new" item. That way the enter/leave animations get fired instead of the move.
I forked your pen to show how it can work: https://codepen.io/josh7weaver/pen/eYOXxed?editors=1010
As you can see, one downside to this approach is that you have to increase the complexity of your data model for it to work. i.e. before you had a simple array, after we have an array of objects since each object needs to be responsible for its own key.
There are many different ways you could generate a unique ID for the item you want to trick vue into thinking is "new," but the method I used I just copied and pasted from a public gist.
Hope it helps!
As far as I'm aware:
Enter is triggered when a new item is added
Leave is triggered when an item is removed
Move is triggered when the order changes
If you want to only trigger an enter to leave transition you would have to add/remove an item from the array instead of shuffle.
Here is an example:
shuffle: function () {
this.items = [1,3,5,7,9];
}
Related
I'm relatively new to Vue so I may not be doing this in the most performant way. I have a table of rows created with a v-for loop. I also have a corresponding list of row titles also created with a v-for loop. They need to be separate DOM elements to enable fixed header scrolling. When I hover over the row I want to change its color AND the color of the corresponding row title.
So I thought you would be able to do the following with Vue:
On mouseover set a data property to the index of the element being hovered over.
Bind a class to the elements of interest to change their styles when the index is equal to their index from the v-for loop.
This does work but it results in very slow / laggy code. I have quite a few rows (40) and columns (60) but it isn't an enormous amount.
An abridged version of the code is below:
Template html
<div class="rowTitle"
v-for="(rowName, i) in yAxis.values" :key="i"
#mouseover="hover(i)"
:class="hoverActive(i)" >{{rowName}}</div>
<div class="row"
v-for="(row, i) in rows" :key="i"
#mouseover="hover(i)"
:class="hoverActive(i)" >{{row}}</div>
Vue object
export default {
data:()=>(
{
rows: rows,
yAxis: yAxis,
rowHover: -1,
}
),
methods:{
hover(i) {
this.rowHover = i
},
hoverActive(i) {
return this.rowHover == i ? 'hover' : ''
}
},
}
The likely answer is that the hover event is firing very frequently, and each time it fires you are forcing vue to re-render. If you could add a debounce the event handler, or even better mark the event handler as passive, it would make a big difference in performance. In this runnable snippet, I'm telling vue to use a passive event modifier (see https://v2.vuejs.org/v2/guide/events.html#Event-Modifiers) to tell the browser not to prioritize that event handler over critical processes like rendering.
To be clear, if you can use CSS to solve the problem like a commenter mentioned (see https://developer.mozilla.org/en-US/docs/Web/CSS/:hover), I strongly advise that as the browser's performance should be excellent with that approach. But sometimes css does not solve everything, so a debounce or a passive event listener might be your next best option. You could also experiment with the "stop" (stop propagation) modifier if additional tuning is required.
const grid = [];
for (var i = 0; i < 40; i++) {
grid.push([]);
for (var j = 0; j < 60; j++) {
grid[i].push({
i,
j
});
}
}
Vue.component('aTable', {
props: ['grid'],
data() {
return {
rowHover: -1
}
},
template: `
<div class="table">
<div class="row" v-for="(row, x) in grid">
<span v-for="cell in row"
#mouseover.passive="hover(x)"
:class="hoverActive(x)"
class="row-cell"
>i:{{cell.i}},j:{{cell.j}}</span>
</div>
</div>
`,
methods: {
hover(i) {
this.rowHover = i
},
hoverActive(i) {
return this.rowHover == i ? 'hover' : ''
}
}
})
var app = new Vue({
el: '#app',
data() {
return {
grid
}
}
})
body {
font-size: 11px;
}
.row-cell {
min-width: 40px;
display: inline-block;
border: 1px solid gray;
}
.row-cell.hover {
color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
My table
<a-table :grid="grid" />
</div>
I would like to add and remove 'over' from my class on an element created using a lit-html template triggered by 'dragEnter' and 'dragLeave':
#app {
background-color: #72a2bc;
border: 8px dashed transparent;
transition: background-color 0.2s, border-color 0.2s;
}
#app.over {
background-color: #a2cee0;
border-color: #72a2bc;
}
const filesTemplate = () =>
html`
<button id="app"
#dragover=${??}
#dragleave=${??}
>
Click Me
</button>
`;
In my old system I called these methods in a separate module via an event emitter, but I am hoping I can make it all defined in the template using lit-html.
dragEnter(e) {
this.view.element.className += ' over';
}
dragLeave(e) {
this.view.element.className = element.className.replace(' over', '');
}
It depends what your custom element looks like. With your template you could just put #dragover=${this.dragEnter}. However, if you want this to apply to your entire custom element and not just the button you can do something like this:
connectedCallback() {
super.connectedCallback();
this.addEventListener('dragover', this.dragEnter);
}
If you do not have custom element and just use lit-html by itself you have to put your event handlers dragEnter(e)and dragLeave(e) into the template like so: #dragover=${this.dragEnter}
You need to add the class with classList.add in dragEnter and remove it in dragLeave. In the future you maybe can use classMap directive in lit-html, however there is nothing wrong with just using classList. I would stick with just using classList. In a very distant future css might also have a selector for it: Is there a CSS ":drop-hover" pseudo-class?
I think that, in order to solve the problem in a "lit-html style", the solution has to be something like this:
import { html, render} from 'lit-html';
import { classMap } from 'lit-html/directives/class-map.js';
const myBtnClasses = {
over: false
};
function dragEnter(e) {
myBtnClasses.over = true;
renderFiles();
}
function dragLeave(e) {
myBtnClasses.over = false;
renderFiles();
}
const filesTemplate = (classes) =>
html`
<button id="app" class="${classMap(myBtnClasses)}"
#dragover=${dragEnter} #dragleave=${dragLeave}
>
Click Me
</button>
`;
function renderFiles() {
render(filesTemplate(myBtnClasses), YOUR_CONTAINER);
}
When using lit-html you have to express your UI as a function of your "state" and "rerender" each time your state changes, so the best solution in this little example is to consider your classes as part of your state.
Anyway better than
this.view.element.className += ' over';
is
this.view.element.classList.add('over');
And instead
this.view.element.className = element.className.replace(' over', '');
use
this.view.element.classList.remove('over');
This is better because of allowing to avoid many bugs like adding the same class many times.
I do not know lit-html but try
let sayHello = (name, myClass) => html`<h1 class="${myClass}">Hello ${name}</h1>`;
https://lit-html.polymer-project.org/
Suppose I want to use the components to make a list that would disappear if I click on it, and use the transition-group to do the animation part.
The following code can perform well:
HTML:
<transition-group name="testanim">
<p key="1" v-if='open1' #click='open1 = false'>Can You See Me?</p>
<p key="2" v-if='open2' #click='open2 = false'>Can You See Me?</p>
</transition-group>
CSS:
.testanim-enter-active, .testanim-leave-active {
transition: all .5s;
}
.testanim-enter, .testanim-leave-to {
transform: translateX(1rem);
opacity: 0;
}
.testanim-leave-active {
position: absolute;
}
.testanim-move {
transition: all .5s;
}
open1 and open2 are defined in data in Vue.js.
However, the following code would not perform the animation at all.
HTML:
<transition-group name="testanim">
<test-sth key="1"></test-sth>
<test-sth key="2"></test-sth>
</transition-group>
CSS: the same with above
JavaScript:
Vue.component ("test-sth", {
template: "<p v-if='open' #click='open = !open'>Can You See Me?</p>",
data: function () {
return {
open: true,
}
}
})
So the problem is that how I can animate the components inside the transition-group. I've searched for a few hours but did not find some question or documents related to it.
Update:
The key problem is that the animation in the former example that the second sentence move upwards smoothly when the first sentense disappear do not show in the latter one. Although I may put the transition inside the template, That do not solve the problem.
Should I write the whole transition-groupinside the template, or something else...?
When using Vue transitions, for internal reasons, the transition/transition-group components must be in the same template as the state that's being toggled.
Also, Vue components require that there always be a single root element for a component. A v-if breaks this rule because it gives the possibility of the element not being there, if the v-if happens to be false.
To solve your issue, move the transitioning to the test-sth component. Since it manages its own toggling, it should manage its own transitioning as well.
Vue.component("test-sth", {
template: `
<transition name='testanim'>
<p v-if='open' #click='open = !open'>Can You See Me?</p>
</transition>
`,
data: () => ({
open: true,
}),
})
new Vue({
el: "#app",
template: `
<div>
<test-sth></test-sth>
<test-sth></test-sth>
</div>
`,
})
See this fiddle for a working example.
I have a relatively simple use case involving a group of buttons each representing a category.
When one of these category buttons is clicked its corresponding sub-categories will appear in a section below. When you click on one of these category buttons, the list of of sub categories gets replaced with new sub-categories depending on the category you just clicked.
An example could be (if I were to click Category A):
[Category A(Clicked)] [Category B] [Category C]
_______________________________________
[Sub-Category A1] [Sub-Category A2] [Sub-Category A3]
Now the problem happens when I click a different category after already clicking one. I notice that the Sub-Categories of the previous action persist in view for a brief amount of time. For instance this is a snapshot of what happens when I click on Category B after already clicking on Category A.
[Category A] [Category B (Clicked)] [Category C]
_______________________________________
[Sub-Category B1] [Sub-Category B2] [Sub-Category B3] [Sub-Category A1]
Even though I clicked on Category B, and the list of Sub-Categories get replaced, the elements from the previous list of Sub-Categories (Category A) stay mounted for a brief amount of time (in the hundreds of milliseconds).
This only happens when I wrap the sub-categories section in the ReactCSSTransitionGroup tag. My keys for each sub-category is the string name of the sub-category, as they are all unique in my database so I don't think my problem is related to the keys being set.
Here is the code for my ReactCSSTransitionGroup markup:
<ReactCSSTransitionGroup transitionName="products" transitionEnterTimeout={0} transitionLeaveTimeout={0}>
{
this.state.subCategories != null
?
this.state.subCategories.map((subCategoryName, index) => {
return(
<div className="product-sub-category" key={subCategoryName}>
<div className="product-image windows"><div className="overlay"><div className="wrap"><div className="category-title">{subCategoryName}</div><div className="category-subcategories">Sub-Categories: <span className="num">1</span></div></div></div></div>
</div>)
})
:
null
}
</ReactCSSTransitionGroup>
Here is the animation code in my animations css file:
.products-enter {
opacity: 0.01;
}
.products-enter.products-enter-active {
opacity: 1;
transition: opacity 100ms ease-in;
}
.products-leave {
opacity: 1;
}
.products-leave.products-leave-active {
opacity: 0.01;
transition: opacity 100ms ease-in;
}
Again, this only happens when using ReactCSSTransitionGroup. And it is visually unpleasing. Even though the fades are technically working, it is totally ruined by the fact that old elements are lingering for a short amount of time instead of being unmounted properly.
That's by design. Fade in/out with ReactCSSTransitionGroup is most appropriate for a list of items that are independently inserted and deleted. For your use case it doesn't work well.
Maybe there's a better library out there, but it's pretty easy to roll your own 'FadeIn' component that simply animates on show, but not hide. It would probably be more appropriate for your use case - here's what i did:
import React, {PropTypes as T} from 'react'
import classnames from 'classnames'
export default class FadeIn extends React.Component {
constructor(...args) {
super(...args)
this.state = {active: false}
}
componentDidMount() {
setTimeout(() => this.setState({active: true}), 0)
}
render() {
const {active} = this.state
return (
<div className={classnames('fade-in', {active}, this.props.className)}>
{this.props.children}
</div>
)
}
}
FadeIn.propTypes = {
className: T.string
}
FadeIn.displayName = 'FadeIn'
LESS:
.fade-in {
opacity: 0;
&.active {
opacity: 1;
transition: opacity 0.15s ease-in;
}
}
I am using Aurelia.js for my UI. Let's say I have the following view markup:
<tr repeat.for="item in items">
<td>${item.name}</td>
<td>${item.value}</td>
</tr>
Which is bound to a model "items". When one of the values in the model changes, I want to animate the cell where the changed value is displayed. How can I accomplish this?
This can be done with Aurelia custom attributes feature.
Create a new javascript file to describe the attribute (I called the attribute "animateonchange"):
import {inject, customAttribute} from 'aurelia-framework';
import {CssAnimator} from 'aurelia-animator-css';
#customAttribute('animateonchange')
#inject(Element, CssAnimator)
export class AnimateOnChangeCustomAttribute {
constructor(element, animator) {
this.element = element;
this.animator = animator;
this.initialValueSet = false;
}
valueChanged(newValue){
if (this.initialValueSet) {
this.animator.addClass(this.element, 'background-animation').then(() => {
this.animator.removeClass(this.element, 'background-animation');
});
}
this.initialValueSet = true;
}
}
It receives the element and CSS animator in constructor. When the value changes, it animates the element with a predefined CSS class name. The first change is ignored (no need to animate on initial load). Here is how to use this custom element:
<template>
<require from="./animateonchange"></require>
<div animateonchange.bind="someProperty">${someProperty}</div>
</template>
See the complete example in my blog or on plunkr
The creator of the crazy Aurelia-CSS-Animator over here :)
In order to do what you want you simply need to get hold of the DOM-Element and then use Aurelia's animate method. Since I don't know how you're going to edit an item, I've just used a timeout inside the VM to simulate it.
attached() {
// demo the item change
setTimeout( () => {
let editedItemIdx = 1;
this.items[editedItemIdx].value = 'Value UPDATED';
console.log(this.element);
var elem = this.element.querySelectorAll('tbody tr')[editedItemIdx];
this.animator.addClass(elem, 'background-animation').then(() => {
this.animator.removeClass(elem, 'background-animation')
});
}, 3000);
}
I've created a small plunkr to demonstrate how that might work. Note this is an old version, not containing the latest animator instance, so instead of animate I'm using addClass/removeClass together.
http://plnkr.co/edit/7pI50hb3cegQJTXp2r4m
Also take a look at the official blog post, with more hints
http://blog.durandal.io/2015/07/17/animating-apps-with-aurelia-part-1/
Hope this helps
Unfortunately the accepted answer didnt work for me, the value in display changes before any animation is done, it looks bad.
I solved it by using a binding behavior, the binding update is intercepted and an animation is applied before, then the value is updated and finally another animation is done.
Everything looks smooth now.
import {inject} from 'aurelia-dependency-injection';
import {CssAnimator} from 'aurelia-animator-css';
#inject(CssAnimator)
export class AnimateBindingBehavior {
constructor(_animator){
this.animator = _animator;
}
bind(binding, scope, interceptor) {
let self = this;
let originalUpdateTarget = binding.updateTarget;
binding.updateTarget = (val) => {
self.animator.addClass(binding.target, 'binding-animation').then(() => {
originalUpdateTarget.call(binding, val);
self.animator.removeClass(binding.target, 'binding-animation')
});
}
}
unbind(binding, scope) {
binding.updateTarget = binding.originalUpdateTarget;
binding.originalUpdateTarget = null;
}
}
Declare your animations in your stylesheet:
#keyframes fadeInRight {
0% {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
100% {
opacity: 1;
transform: none
}
}
#keyframes fadeOutRight {
0% {
opacity: 1;
transform: none;
}
100% {
opacity: 0;
transform: translate3d(-100%, 0, 0)
}
}
.binding-animation-add{
animation: fadeOutRight 0.6s;
}
.binding-animation-remove{
animation: fadeInRight 0.6s;
}
You use it in your view like
<img src.bind="picture & animate">