diff --git a/examples/layout-transitions/README.md b/examples/layout-transitions/README.md new file mode 100644 index 0000000000..3bdd2a9092 --- /dev/null +++ b/examples/layout-transitions/README.md @@ -0,0 +1,3 @@ +# Layout transitions with Nuxt.js + +https://nuxtjs.org/examples/layout-transitions diff --git a/examples/layout-transitions/assets/main.css b/examples/layout-transitions/assets/main.css new file mode 100644 index 0000000000..dbb73ba005 --- /dev/null +++ b/examples/layout-transitions/assets/main.css @@ -0,0 +1,52 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; +} + +.container { + text-align: center; + padding-top: 200px; + font-size: 20px; + transition: all .5s cubic-bezier(.55,0,.1,1); +} + +.page-enter-active, .page-leave-active { + transition: opacity .5s +} +.page-enter, .page-leave-active { + opacity: 0 +} + +.layout-enter-active, .layout-leave-active { + transition: opacity .5s +} +.layout-enter, .layout-leave-active { + opacity: 0 +} + +.bounce-enter-active { + animation: bounce-in .8s; +} +.bounce-leave-active { + animation: bounce-out .5s; +} +@keyframes bounce-in { + 0% { transform: scale(0) } + 50% { transform: scale(1.5) } + 100% { transform: scale(1) } +} +@keyframes bounce-out { + 0% { transform: scale(1) } + 50% { transform: scale(1.5) } + 100% { transform: scale(0) } +} + +.slide-left-enter, +.slide-right-leave-active { + opacity: 0; + transform: translate(30px, 0); +} +.slide-left-leave-active, +.slide-right-enter { + opacity: 0; + transform: translate(-30px, 0); +} diff --git a/examples/layout-transitions/layouts/secondary.vue b/examples/layout-transitions/layouts/secondary.vue new file mode 100644 index 0000000000..d4a9065bd3 --- /dev/null +++ b/examples/layout-transitions/layouts/secondary.vue @@ -0,0 +1,11 @@ +<template> + <div> + <menu> + <ul> + <li>Option 1</li> + <li>Option 2</li> + </ul> + </menu> + <nuxt/> + </div> +</template> diff --git a/examples/layout-transitions/nuxt.config.js b/examples/layout-transitions/nuxt.config.js new file mode 100644 index 0000000000..e1b2d64a6c --- /dev/null +++ b/examples/layout-transitions/nuxt.config.js @@ -0,0 +1,10 @@ +module.exports = { + build: { + vendor: ['axios'] + }, + css: ['~/assets/main.css'], + layoutTransition: { + name: 'layout', + mode: 'out-in' + } +} diff --git a/examples/layout-transitions/package.json b/examples/layout-transitions/package.json new file mode 100644 index 0000000000..38fd64e26c --- /dev/null +++ b/examples/layout-transitions/package.json @@ -0,0 +1,12 @@ +{ + "name": "nuxt-layout-transitions", + "dependencies": { + "axios": "^0.15.3", + "nuxt": "latest" + }, + "scripts": { + "dev": "../../bin/nuxt", + "build": "nuxt build", + "start": "nuxt start" + } +} diff --git a/examples/layout-transitions/pages/about.vue b/examples/layout-transitions/pages/about.vue new file mode 100644 index 0000000000..0ec094454f --- /dev/null +++ b/examples/layout-transitions/pages/about.vue @@ -0,0 +1,13 @@ +<template> + <div class="container"> + <h1>About page</h1> + <nuxt-link to="/">Home page</nuxt-link> + </div> +</template> + +<script> +export default { + layout: 'secondary', + transition: 'bounce' +} +</script> diff --git a/examples/layout-transitions/pages/index.vue b/examples/layout-transitions/pages/index.vue new file mode 100644 index 0000000000..fe5ce74005 --- /dev/null +++ b/examples/layout-transitions/pages/index.vue @@ -0,0 +1,7 @@ +<template> + <div class="container"> + <h1>Home page</h1> + <p><nuxt-link to="/about">About page</nuxt-link></p> + <p><nuxt-link to="/users">Lists of users</nuxt-link></p> + </div> +</template> diff --git a/examples/layout-transitions/pages/users.vue b/examples/layout-transitions/pages/users.vue new file mode 100644 index 0000000000..ce5c065ba4 --- /dev/null +++ b/examples/layout-transitions/pages/users.vue @@ -0,0 +1,71 @@ +<template> + <div class="container"> + <nuxt-link v-if="page > 1" :to="'?page=' + (page - 1)">< Prev</nuxt-link> + <a v-else class="disabled">< Prev</a> + <span>{{ page }}/{{ totalPages }}</span> + <nuxt-link v-if="page < totalPages" :to="'?page=' + (page + 1)">Next ></nuxt-link> + <a v-else class="disabled">Next ></a> + <ul> + <li v-for="user in users"> + <img :src="user.avatar" class="avatar" /> + <span>{{ user.first_name }} {{ user.last_name }}</span> + </li> + </ul> + <p><nuxt-link to="/">Back home</nuxt-link></p> + </div> +</template> + +<script> +import axios from 'axios' + +export default { + transition (to, from) { + if (!from) return 'slide-left' + return +to.query.page < +from.query.page ? 'slide-right' : 'slide-left' + }, + async asyncData ({ query }) { + const page = +query.page || 1 + const { data } = await axios.get(`https://reqres.in/api/users?page=${page}`) + return { + page: +data.page, + totalPages: data.total_pages, + users: data.data + } + } +} +</script> + +<style scoped> +a { + display: inline-block; + margin: 0 1em; + color: #34495e; + text-decoration: none; +} +a.disabled { + color: #ccc; +} +ul { + margin: auto; + padding: 0; + width: 100%; + max-width: 400px; + padding-top: 40px; +} +li { + list-style-type: none; + width: 400px; + border: 1px #ddd solid; + overflow: hidden; +} +li img { + float: left; + width: 100px; + height: 100px; +} +li span { + display: inline-block; + padding-top: 40px; + text-transform: uppercase; +} +</style> diff --git a/lib/app/App.vue b/lib/app/App.vue index 37e6d69b37..53806cd92e 100644 --- a/lib/app/App.vue +++ b/lib/app/App.vue @@ -1,7 +1,9 @@ <template> <div id="__nuxt"> <% if (loading) { %><nuxt-loading ref="loading"></nuxt-loading><% } %> - <component v-if="layout" :is="nuxt.err ? 'nuxt' : layout"></component> + <% if (layoutTransition) { %><transition name="<%= layoutTransition.name %>" mode="<%= layoutTransition.mode %>"><% } %> + <component v-if="layout" :is="nuxt.err ? 'nuxt' : layout"></component> + <% if (layoutTransition) { %></transition><% } %> </div> </template> diff --git a/lib/app/components/nuxt-child.js b/lib/app/components/nuxt-child.js index 39b8e3c4e7..5e17edbea1 100644 --- a/lib/app/components/nuxt-child.js +++ b/lib/app/components/nuxt-child.js @@ -37,6 +37,19 @@ export default { name: 'nuxt-child', functional: true, render (h, { parent, data }) { + const nuxt = parent.$root.nuxt + const component = parent.$route.matched[0].components.default + + const layoutUid = parent._uid + const layoutName = component.options ? component.options.layout : null + + // If we're changing layout return the stored vnode + if (nuxt._layoutUid === layoutUid && + nuxt._layoutName !== layoutName) return nuxt._layoutVnode + + nuxt._layoutUid = layoutUid + nuxt._layoutName = layoutName + data.nuxtChild = true const _parent = parent const transitions = parent.$nuxt.nuxt.transitions @@ -62,11 +75,14 @@ export default { listeners[key] = transition[key].bind(_parent) } }) - return h('transition', { + + nuxt._layoutVnode = h('transition', { props: transitionProps, on: listeners }, [ h('router-view', data) ]) + + return nuxt._layoutVnode } } diff --git a/lib/builder/builder.js b/lib/builder/builder.js index cf1de02253..ec84c59138 100644 --- a/lib/builder/builder.js +++ b/lib/builder/builder.js @@ -219,6 +219,7 @@ export default class Builder extends Tapable { layouts: Object.assign({}, this.options.layouts), loading: typeof this.options.loading === 'string' ? this.relativeToBuild(this.options.srcDir, this.options.loading) : this.options.loading, transition: this.options.transition, + layoutTransition: this.options.layoutTransition, components: { ErrorPage: this.options.ErrorPage ? this.relativeToBuild(this.options.ErrorPage) : null } diff --git a/lib/common/options.js b/lib/common/options.js index 8c6f099314..7f0e5b8d32 100755 --- a/lib/common/options.js +++ b/lib/common/options.js @@ -24,6 +24,9 @@ Options.from = function (_options) { if (typeof options.transition === 'string') { options.transition = { name: options.transition } } + if (typeof options.layoutTransition === 'string') { + options.layoutTransition = { name: options.layoutTransition } + } // Apply defaults _.defaultsDeep(options, Options.defaults) @@ -243,6 +246,10 @@ Options.defaults = { appearActiveClass: 'appear-active', appearToClass: 'appear-to' }, + layoutTransition: { + name: 'layout', + mode: 'out-in' + }, router: { mode: 'history', base: '/',