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">
Related
So, my problem is that I have a component, I associated an animation to it and it is working when the component is rendered for the first time, but on an event click I change some conditions and some props associated to this component, But my element is not re rendered, it is just changing what has been changed, that means that the element is not removed from the dom et added to the DOM again, that's why I am not able to see the animation again, so it is not re-rendered or I just did not get what re render means.
I tried some solutions of course, but I am stuck, I tried to use this method :
this.forceUpdate();
But again, I am still not getting anything.
I dont think I have to write the whole code I wrote, becuase it is a lot and includes many other things but This is what I think is needed.
methodWillReceiveProps in my component :
componentWillReceiveProps(props) {
if (props.isRerendered) {
this.forceUpdate();
}
}
props.isRendered is returning true everytime, I checked with some console.log methods.
This is what is rendered :
render() {
return (
<div
className={cs({
"tls-forms": true,
"tls-forms--large": this.props.type === "S",
"tls-forms--medium tls-forms--login": !(this.props.type === "S")
})}
>
// content here
</div>);
}
And here is the sass file and the simple fading animation :
.tls-forms {
animation: formFading 3s;
// childs properties here
}
#keyframes formFading {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
I will really appreciate any help given.
You could make use of keys that react is using to determine whether something has changed. This means that your render method should look something like this:
import shortid from "shortid";
getRandomKey = () => {
return shortid.generate();
}
render() {
return (
<div
key={this.getRandomKey()}
className={cs({
"tls-forms": true,
"tls-forms--large": this.props.type === "S",
"tls-forms--medium tls-forms--login": !(this.props.type === "S")
})}
>
// content here
</div>);
}
Since you need to run animation on each render, you'll need to generate some random key every time (that's why we are calling this.getRandomKey() on each render). You can use whatever you like for your getRandomKey implementation, though shortid is pretty good for generating unique keys.
One way of animating a component is to attach a CSS class to it. But, when animation is done, you have to detach the CSS class so that you can re-attach when you want to animate again.
Here is a basic example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
animateFlag: false
};
}
componentDidUpdate() {
if (this.state.animateFlag) {
setTimeout(() => {
this.setState({ animateFlag: false });
}, 3000);
}
}
render() {
return (
<div className="App">
<button
onClick={() =>
this.setState({ animateFlag: !this.state.animateFlag })
}
>
{this.state.animateFlag ? "Wait" : "Re-animate"}
</button>
<div className={this.state.animateFlag ? "text animate" : "text"}>
Hello CodeSandbox
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.text {
font-size: 40px;
}
.text.animate {
animation: formFading 3s;
}
#keyframes formFading {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Note that, I am setting animateFlag to false in ComponentDidUpdate, so that when I click the Re-animate button again, I can re-attach the animate class to the div element.
I set timeout duration to 3000ms because, the animation takes 3000ms.
I want an overlay to show up when I click a search icon.
I managed to get it working using jQuery. But can't seem to get it working with javascript.
The click event does not seem to be registering and I don't know why.
I've checked all the class names so they match in the same in both the HTML and javascript
Here is the jQuery code that works:
import $ from 'jquery';
class Search {
constructor() {
this.openButton = $('.js-search-trigger');
this.closeButton = $('.search-overlay__close');
this.searchOverlay = $(".search-overlay");
this.events();
}
events() {
this.openButton.on('click', this.openOverlay.bind(this));
this.closeButton.on('click', this.closeOverlay.bind(this));
}
openOverlay() {
this.searchOverlay.addClass("search-overlay--active");
}
closeOverlay() {
this.searchOverlay.removeClass("search-overlay--active");
}
}
export default Search;
Here is the javascript code that does not work:
class Search {
constructor() {
this.openButton = document.querySelector('.js-search-trigger');
this.closeButton = document.querySelector('.search-overlay__close');
this.searchOverlay = document.querySelector('.search-overlay');
this.events();
}
events() {
this.openButton.addEventListener('click', function() {
this.openOverlay.bind(this);
});
this.closeButton.addEventListener('click', function() {
this.closeOverlay.bind(this);
});
}
openOverlay() {
this.searchOverlay.classList.add('search-overlay--active');
}
closeOverlay() {
this.searchOverlay.classList.remove('search-overlay--active');
}
}
export default Search;
No errors were shown in the javascript where the overlay was not showing.
You'll probably want to change your event listeners to use the correct this binding:
this.openButton.addEventListener("click", this.openOverlay.bind(this));
Or use an arrow function to go with your approach - but make sure you actually call the resulting function, as in the above approach the function is passed as a reference and is called. If you removed the additional () from the code below, it would be the same as writing a function out in your code normally - it would be defined, but nothing would happen.
this.openButton.addEventListener("click", () => {
this.openOverlay.bind(this)();
});
jQuery also uses collections of elements rather than single elements, so if you have multiple elements, querySelectorAll and forEach might be in order.
If we are speaking of ecmascript-6 (I see the tag), I would recommend to use arrow function to have this inherited from the above scope, and no bind is needed:
this.openButton.addEventListener('click', () =>
this.openOverlay()
);
The problems with your code are that a) the function creates new scope with its own this; b) bound methods are not being invoked.
Why Search? You're creating an Overlay. Stick with the plan.
No need to bind anything. Use Event.currentTarget if you want to.
No need to handle .open/.close if all you need is a toggle.
And the below should work (as-is) for multiple Overlays. The overlay content is up to you.
class Overlay {
constructor() {
this.toggleButtons = document.querySelectorAll('[data-overlay]');
if (this.toggleButtons.length) this.events();
}
events() {
this.toggleButtons.forEach(el => el.addEventListener('click', this.toggleOverlay));
}
toggleOverlay(ev) {
const btn = ev.currentTarget;
const sel = btn.getAttribute('data-overlay');
const overlay = sel ? document.querySelector(sel) : btn.closest('.overlay');
overlay.classList.toggle('is-active');
}
}
new Overlay();
*{margin:0; box-sizing:border-box;} html,body {height:100%; font:14px/1.4 sans-serif;}
.overlay {
background: rgba(0,0,0,0.8);
color: #fff;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
padding: 5vw;
transition: opacity 0.4s, visibility 0.4s;
visibility: hidden;
opacity: 0;
}
.overlay.is-active {
opacity: 1;
visibility: visible;
}
<button type="button" data-overlay="#search">OPEN #search</button>
<button type="button" data-overlay="#qa">OPEN #qa</button>
<div class="overlay" id="search">
<button type="button" data-overlay>CLOSE</button>
<h2>SEARCH</h2>
<input type="text" placeholder="Search…">
</div>
<div class="overlay" id="qa">
<button type="button" data-overlay>CLOSE</button>
<h2>Q&A</h2>
<ul><li>Lorem ipsum</li></ul>
</div>
The above is still not perfect, still misses a way to "destroy" events and not re-attach duplicate events to already initialised buttons when trying to target dynamically created ones.
Also, the use of Classes for the above task is absolutely misleading and unnecessary.
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];
}
I'm using the CDK Overlay to display a "popover" when the user hovers over a list item. I currently open the popover when the mouseenter event fires.
My code:
//component.html
<mat-list-item *ngFor="let item of itemList" (mouseenter)="showItemDetail(item)">
{{item.display}}
</mat-list-item>
//component.ts
showItemDetail(item: IItemDto, event: MouseEvent) {
this.hideItemDetail(); // Closes any open overlays
this.itemDetailOverlayRef = this.itemDetailOverlayService.open(item);
}
//itemDetailOverlayService.ts
open(item: IItemDto) {
// Returns an OverlayRef (which is a PortalHost)
const overlayRef = this.createOverlay(item);
const dialogRef = new ItemDetailOverlayRef(overlayRef);
// Create ComponentPortal that can be attached to a PortalHost
const itemDetailPortal = new ComponentPortal(ItemDetailOverlayComponent);
const componentInstance = this.attachDialogContainer(overlayRef, item, dialogRef);
// Attach ComponentPortal to PortalHost
return dialogRef;
}
private attachDialogContainer(overlayRef: OverlayRef, item: IItemDto, dialogRef: ItemDetailOverlayRef) {
const injector = this.createInjector(item, dialogRef);
const containerPortal = new ComponentPortal(ItemDetailOverlayComponent, null, injector);
const containerRef: ComponentRef<ItemDetailOverlayComponent> = overlayRef.attach(containerPortal);
return containerRef.instance;
}
Note that my overlay is dependent on data from list item data.
How can I delay showItemDetail() to only open the overlay after 2s? Keep in mind that only one popover can be open at a time.
setTimeout() obviously won't work as multiple popovers will be opened if the user drags the mouse across the list of items.
Resolved by opening the overlay without delay while creating the delay effect using css animation/keyframes:
.container {
animation: fadeIn 1.5s linear;
}
#keyframes fadeIn {
0% {
opacity: 0;
}
75% {
opacity: 0;
}
100% {
opacity: 1;
}
}
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/