justintaddei / v-shared-element

Declarative shared-element transitions for Vue.js
https://justintaddei.github.io/v-shared-element/
MIT License
465 stars 16 forks source link
animation route-transitions shared-element shared-element-transition vue vue-router

Vue logo

v-shared-element

checks

Declarative shared-element transitions between pages for Vue.js.
Uses illusory under the hood.

v3.1.0 released!

Vue 3 is now supported with Vue 2 backwords compatibility
Special thanks to @domgew for #26

Example page

Source code for the example can be found on the example branch.

gif of example page


Index

Install

npm

$ npm i v-shared-element

or

CDN

<script src="https://unpkg.com/illusory"></script>
<script src="https://unpkg.com/v-shared-element"></script>

Register the plugin

Vue.js + vue-router (Vue 2)

//main.js

import Vue from 'vue'
import {
    SharedElementRouteGuard,
    SharedElementDirective,
    createSharedElementDirective
} from 'v-shared-element'

Vue.use(SharedElementDirective)

const router = new VueRouter({ ... })

router.beforeEach(SharedElementRouteGuard)

or

Vue.js + vue-router (Vue 3)

//main.ts

import { createApp } from 'vue'
import {
    createSharedElementDirective,
    SharedElementRouteGuard,
    SharedElementDirective
} from 'v-shared-element'

const app = createApp(App)

app.use(SharedElementDirective)
// or app.use(createSharedElementDirective())

const router = new VueRouter({ ... })

router.beforeEach(SharedElementRouteGuard)

or

Nuxt.js

Simply add it as a module in your nuxt.config.js

// nuxt.config.js

export default {
  modules: ['v-shared-element/nuxt'],
}

Usage

Add v-shared-element:<namespace> to an element to transform it into a shared-element. On another page add the directive to an element and use the same namespace. That's it, you're done (there are more options below if you need them).

Note: A given namespace should only be used once per-page. See below for usage with v-for.
Also, keep-alive routes need special treatment (see below).

<div v-shared-element:namespace></div>

Usage with v-for

Suppose you have a list of contacts and you want all the profile pictures to be shared-elements.
Use "dynamic directive arguments" to give a different namespace to each contact in the list (this is typically the same ID used for v-for's :key prop).

<img
  :src="https://github.com/justintaddei/v-shared-element/raw/master/contact.profile"
  v-shared-element:[contact.id]
/>

contact example gif

Detailed example ```html ```

Usage with keep-alive

If you have routes that use <keep-alive>, you must add some additional code. Otherwise, the transition will only run once, and not run again while the component remains alive.

To fix this, use sharedElementMixin on routes that are "kept alive".

Using sharedElementMixin

Import the mixin into any components on your keep-alive routes that contain shared-elements. Then, in those components, pass $keepSharedElementAlive—a method provided by the mixin—as an option to every v-shared-element directive on that route. Everything should now work as you would expect.

Note: This is only necessary for routes that are kept alive. For example, if /home is kept alive but /about is not, then only /home needs to import the mixin.

keep-alive example
```html ```

Options

Options can be applied globally (when you register the plugin) and/or on each individual shared-element.

A note on option hierarchy

Setting global options

Vue.js + vue-router (Vue 2)

// main.js

Vue.use(SharedElementDirective, {
  /* options */
});

or

Vue.js + vue-router (Vue 3)

// main.ts

app.use(SharedElementDirective, {
  /* options */
});

// or

app.use(createSharedElementDirective({
  /* options */
}))

or

Nuxt.js

// nuxt.config.js

export default {
  modules: ['v-shared-element/nuxt'],

  vSharedElement: {
   /* options */
  }
}

Setting per-element options

<img
  src="https://github.com/justintaddei/v-shared-element/raw/master/logo.png"
  v-shared-element:logo="{
    /* options */
  }"
/>

Summary

option type default
easing string "ease"
duration string "300ms"
endDuration string "150ms"
zIndex number 1
compositeOnly boolean false
includeChildren boolean true
ignoreTransparency boolean \| string[] ["img"]
restrictToViewport boolean true
restrictToRoutes boolean false

Details

easing

duration

endDuration

zIndex

compositeOnly

includeChildren

ignoreTransparency

restrictToViewport

restrictToRoutes

Usage with page transitions

Thanks to @719media for figuring out how to make this work.

This section assumes you have an understanding of Vue's transition component.

In your CSS

.page-enter-active {
  transition: opacity 150ms ease 150ms;
}

.page-leave-active {
  position: absolute;
  transition: opacity 150ms ease;
}

.page-leave-to,
.page-enter {
  opacity: 0;
}

For Vue.js + vue-router

// App.vue (or equivalent)

<div id="app">
  <transition
    name="page"
    @before-leave="beforeLeave"
    @after-leave="afterLeave"
  >
    <router-view></router-view>
  </transition>
</div>

<script>
export default {
  methods: {
    beforeLeave(el) {
      const {top} = el.getBoundingClientRect();
      el.style.position = "fixed";
      el.style.top = `${top}px`;
      el.style.left = 0;
      el.style.right = 0;
      el.style.zIndex = '-1';
    },
    afterLeave(el) {
      el.style.position = '';
      el.style.top = '';
      el.style.left = '';
      el.style.right = '';
      el.style.zIndex = '';
    }
  }
}
</script>

or

For Nuxt.js

// nuxt.config.js

export default {
  pageTransition: {
    name: 'page',
    mode: '',
    beforeLeave(el) {
      const {top} = el.getBoundingClientRect();
      el.style.position = "fixed";
      el.style.top = `${top}px`;
      el.style.left = 0;
      el.style.right = 0;
      el.style.zIndex = '-1';
    },
    afterLeave(el) {
      el.style.position = '';
      el.style.top = '';
      el.style.left = '';
      el.style.right = '';
      el.style.zIndex = '';
    }
  }
}

Important note about page transitions

If the total duration of the page transition is longer than the duration of a shared-element on that page, things will get weird. You have been warned.

illusory

v-shared-element derives its element-morphing powers from its sister project illusory.

illusory comes bundled with v-shared-element as Vue instance methods.
For more information on how to use it, see the illusory documentation or the illusory example page.

illusory is exposed on the Vue instance as $illusory and $createIllusoryElement.

For example:

<template>
  <div>
    <div ref="from"></div>
    <div ref="to"></div>
    <button @click="morph">Morph!</button>
  </div>
</template>

<script>
  export default {
    methods: {
      morph() {
        this.$illusory(this.$refs.from, this.$refs.to)
      }
    }
  }
</script>

Asking questions and reporting bugs

If you're experiencing any problems, or have general questions about the plugin, feel free open a new issue (but search through the existing ones first, as your question may have been answered already).

Note that issues related to the $illusory and $createIllusoryElement should be opened on the illusory repository instead.

How to contributing

Development setup

  1. Fork and clone the repo.

    $ git clone https://github.com/<your username>/v-shared-element.git
  2. Install the dependencies

    $ npm install
  3. Create a new branch for your pull request

    $ git checkout -b pr/your-branch-name

Common NPM Scripts

Web page for development

Run npm link so you can use it in other local projects.

Method one
You can either create a new Vue.js or Nuxt.js project and use npm link v-shared-element to test your changes.

or

Method two
Clone the repo again, this time into a new directory. Then and run the following:

$ git checkout example
$ npm install
$ npm link v-shared-element
$ npm run dev

You should now have the example page running on localhost.
Hot reload will be triggered by changes made to v-shared-element.

See CONTRIBUTING.md for info.

License

This project is distributed under the MIT License.

The MIT License (MIT)

Copyright (c) 2020 Justin Taddei

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.