Skip to content

Instantly share code, notes, and snippets.

@busster
Created September 17, 2017 22:36
Show Gist options
  • Save busster/d778704c6a6f55e3c150415e44092830 to your computer and use it in GitHub Desktop.
Save busster/d778704c6a6f55e3c150415e44092830 to your computer and use it in GitHub Desktop.

Transitions / Animations Part 2 (Javascript Hooks)

This is the second part to Baby Steps Vuejs Animations Tutorial. In this tutorial we will learn about creating functional and reusable vuejs transitions using javascript.

This app is a simple illustration of the two types of transitions we learned in the previous tutorial - single element transitions and group transitions. The base state of this app contains two components, single-transition.js and group-transition.js. These components contain simple CSS fade transitions. We will go through the steps to convert these to javascript transitions!

Basics

In the previous tutorial we learned how to create transitions using the CSS hooks that the vuejs transition component provided us. These included:

  • v-enter - starting state for enter
  • v-enter-active - active state for enter
  • v-enter-to - ending state for enter
  • v-leave - starting state for leave
  • v-leave-active - active state for leave
  • v-leave-to - ending state for leave

Similar to the CSS hooks, the transition component also provides javascript hooks:

  • v-on:before-enter
  • v-on:enter
  • v-on:after-enter
  • v-on:enter-cancelled
  • v-on:before-leave
  • v-on:leave
  • v-on:after-leave
  • v-on:leave-cancelled

Let's take a second to understand what these mean exactly. Let's take a look at v-on:before-enter. There are two key things happening here:

  1. v-on
  2. before-enter

v-on is a directive, and its job is to listen to the DOM for events. before-enter is the event that it is listening for! So, the take away here is that the transition component emits certain events when it is activated, these being before-enter, enter, after-enter, etc... and we can use the v-on directive to listen for these events, and trigger methods in the component when they occur.

But what about the CSS hooks? If we are creating a javascript only transition, we wouldn't want the transition component to also apply CSS hooks to the DOM element. That leaves the possibility for CSS rules to interfere with the transition. To solve this, we can use v-bind:css=“false” to tell Vue it can skip the CSS detection.

With this in mind, let's jump into our app and see how these things work!

Step-1 First Javascript Transition

Let's start with our single-transition component, and convert the CSS fade transition to a javascript one. First, we should tell Vue that we are going to be creating a purely javascript transition by adding the binded css value.

...
<transition
  name="fade"
  v-bind:css="false"
>
  <div v-if="showText" class="text">Bob's Burgers</div>
</transition>
...

<!-- single-transition.js -->

This, as we know, will tell Vue to skip the CSS hooks.

So how should we create this fade effect? Taking a look at the CSS we used to create the fade transition, we can see there were two parts:

  1. the end state opacity: 0
  2. the active state transition: opacity 1s

So similarly, we know that on transition enter (v-on:enter) and on transition leave (v-on:leave) we will have to tell our element to go from no opacity to full and vice versa. There are many ways to implement transitions using javascript.

Plain Javascript:

function fadeOut(el){
  el.style.opacity = 1
  (function fade() {
    if ((el.style.opacity -= .05) < 0) {
      el.style.display = "none"
      // done!
    } else {
      requestAnimationFrame(fade);
    }
  })()
}

jQuery:

$(el).fadeOut("slow", () => {
    // done!
})

Velocity:

Velocity(el, "fadeOut", {complete: () => {
    // done!
}})

Any of these methods will work, but I prefer to use Velocity. It uses the same api as jQuery's $.animate(), but is much faster. So for this tutorial we will be creating our animation effects with Velocity.

So let's wire up some transitions! Let's create our fade in and fade out methods. To mirror the Vuejs transition api, we will name them enter and leave.

...
enter(el, done) {
    Velocity(el, "fadeIn", done)
},

leave(el, done) {
    Velocity(el, "fadeOut", done)
}
...

// single-transition.js

Some things to note here. In order to execute this transition, we need to tell Velocity what element to perform the animation on. When the transition component emits the enter event, it passes two arguments along:

  1. The first being the element that is in transition.
  2. The second is the done callback

Note that the done callback is only used in the enter and leave hooks. If they are not used, they will be called synchronously and the transition will finish immediately.

Gotchas aside, see how easy it is to create fade transitions! We still need to do one more thing though, and that's to tell Vue to execute our enter method on the enter hook.

...
<transition
  name="fade"
  v-bind:css="false"
  v-on:enter="enter"
  v-on:leave="leave"
>
  <div v-if="showText" class="text">Bob's Burgers</div>
</transition>
...

<!-- single-transition.js -->

There ya have it! A fully function fade transition using javascript.

Step-2 Group Transitions Using Javascript

Now that we have converted our fade transition to a pure JS one, let's see how we can do the same thing for our group-transition.

We are going to create a horizontal slide transition. There are many ways to achieve this, but one simple way is just to animate the margin of the element. Of course this might not work as intended if the DOM element already has some sort of margin applied to it, but in our case it'll do just fine! We can explore some other ways to achieve similar affects later. To achieve this affect we know we will need to set some intial margin, and then animate from that state to no margin.

So first things first, let's create our methods.

...
beforeEnter(el) {
    el.style.marginLeft = '10px'
    el.style.opacity = 0
},

beforeLeave(el) {
    el.style.position = 'absolute'
},

enter(el, done) {
    Velocity(el, {marginLeft: 0, opacity: 1}, done)
},

leave(el, done) {
    Velocity(el, {marginLeft: '10px', opacity: 0}, done)
},
...

// group-transition.js

Here we are defining four methods beforeEnter, beforeLeave, enter, and leave. beforeEnter is useful for setting up the initial state of the transition. We know that it is hidden (opacity = 0), and that it is pushed over to the right (marginLeft = '10px'). On enter we will be animating both of these properties to their shown states, and on leave we will animate them back! The last method beforeLeave simply sets the position style attribute to 'absolute' so that it is removed from the flow of the list and the other elements can slide gracefully to their new positions in the list.

One limitation to creating transitions using javascript is that there is no hook for v-move. We used this in our CSS transitions for animating effects like shuffling the list, and for sliding elements to their new places when an item was removed. Until the api is updated, we must maintain this effect with a CSS hook.

The final step here is to wire up these methods to the transition component!

...
<transition-group
    name="list"
    v-bind:css="false"
    v-on:before-enter="beforeEnter"
    v-on:before-leave="beforeLeave"
    v-on:enter="enter"
    v-on:leave="leave"
>
    <div v-for="item in items" v-bind:key="item.id" class="item">
        <p class="item-text">id: {{ item.id }}</p>
        <p class="item-text">text: {{ item.text }}</p>
    </div>
</transition-group>
...

<!-- group-transition.js -->

That's all there is to it! Now you have successfully created group transitions using javascript.

Step-3 Functional & Reusable Javascript Components

One problem with creating transitions this way is that we are adding methods to our components that don't really have anything to do with their functionality. This is not a big deal in smaller components, but you can imagine that as your components get more complex, these methods would clutter up the business logic. To remedy this, we can abstract our transitions to their own components! Let's do that now starting with the single fade transition.

Let's create a file called fade-transition.js

This component will have all of the same methods that we used in our single-transition component, but will look a little different to all of the components we've written so far. We will write our fade-transition as a functional component.

Why functional components? Functional components are stateless (no data attribute) and instanceless (no lifecycle hooks) components. They are really just functions that take some props. If you think about our transition, this is pretty much exactly what it does - performs actions on child elements based on events.

There is a lot of information about functional components to distill. Since our fade-transition.js component is fairly small, let's look at it in its entirety and then zero in on the important parts.

((() => {
  Vue.component("fade-transition", {
    functional: true,
    render: (createElement, context) => {
      let data = {
        props: {
          name: "fade",
          css: false
        },
        on: {
          enter(el, done) {
            Velocity(el, "fadeIn", done)
          },

          leave(el, done) {
            Velocity(el, "fadeOut", done)
          }
        },

      }
      return createElement('transition', data, context.children)
    }
  })
}))()

// fade-transition.js
  • The first thing to note is the function: true - this as you probably guessed denotes this as a functional component!
  • The next big change from the components we're used to seeing is the render function. Usually we use a template declaration for passing HTML markup to our component.

The render function is just another way of defining the HTML and will alow us the full power of JavaScript!

The render component takes some arguments, the first one being the createElement function. createElement basically returns a description for Vue on how to render the Virtual DOM element. You can see the last return of our component is createElement('transition', data, context.children). createElement accepts 3 arguments:

  1. The first is an HTML tag name.
  2. The next is a data object that details the attributes you would use in the template.
  3. The last is children Virtual Nodes.

You can learn more about the data object here

For this component though we are mainly concern with defining the behavior of the events triggered by the v-on directive, giving our component a name, and defining the css behavior. We do this in a data object, defining the key of props and its value, and the key of on and its value.

That's the basics of functional components!

Step-4 Dynamic Components

One last important concept is Dynamic Components. Let's say we have an image slider component that has arrows on the left and right sides. When someone clicks the right arrow, the current image slides to the left with a new image from the right sliding into view, and vice versa for clicking the left arrow. How would we be able to achieve this type of transition? Up until now we have explicitly defined the behavior of our transitions. It can either slide up, down, left, right, etc... but is not equipped to perform some or all based on some data.

One solution might be to define the transition's method behaviors based on some property, and pass that data to the transition component.

...
<slide-transition :direction="left">
  <div v-if="showText" class="text">Bob's Burgers</div>
</slide-transition>
...
...
Vue.component("slide-transition", {
    functional: true,
    props: ["direction"],
    render: (createElement, context) => {
...

In the on: portion of the data object, you can then use a conditional to switch behavior based on the value of the prop.

However another solution that might be cleaner is to use Dynamic Components. This just dynamically changes between componets at one mount point. To do this we use the <component> element and bind the value of the component to the is attribute. Simple as that!

...
<component :is="transitionType">
  <div v-if="showText" class="text">Bob's Burgers</div>
</component>
...

data() {
  return {
    transitionType: "fade-transition",
    showText: true
  }
},

...

// single-transition.js

Here is the example in our single-transition component. We declare a dynamic component and bind its value to a data attribute called transitionType. In this way we can change transitionType on the fly, and as long as we have a transition with that name defined, that transition component will be used at the at the mount point of the component.

Now we can define as many different transition types we want, and change them as we see fit! In this app, we bind transitionType to a select box, the options being names of various transition components. Open the finished_app and change the value of the select box to see some example transitions. These are some example ideas for transitions.

Bonus

One interesting hack to note in the finished_app is text-change-transition.js. In this component we want to change the value of the innerText in one javascript hook, and change it back to its original value. One way would be to use a prop, but what if we don't want the transition to be dependant on a prop from the parent component. The hack seen here is to declare a domProps object in which to store this value, and use a reference to this inside the createElement function to reference it.

...
render: (createElement, context) => {
  let self = this
  let data = {
    props: {
      name: "text-change",
      css: false
    },
    domProps: {
      text: ""
    },
    on: {
      beforeEnter(el) {
        self.text = el.innerText
        el.innerText = "Time for the charm bomb to explode."
      },
      
      enter(el, done) {
        Velocity(el, "fadeIn", {duration: 500, complete: () => {
          Velocity(el, "fadeOut", {duration: 250, complete: () => {
            el.innerText = self.text
            Velocity(el, "fadeIn", {duration: 500, complete: done})
          }})
        }})
      },
...

// text-change-transition.js

Something to keep in mind if you ever need a quick and easy way to keep track of some value in between javascript hooks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment