mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 16:12:12 +00:00
Merge pull request #77 from nuxt/feature-layout
Layouts feature + bug fixes
This commit is contained in:
commit
593e12f348
60
examples/custom-layouts/README.md
Normal file
60
examples/custom-layouts/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Layouts
|
||||
|
||||
> Nuxt.js allows you to extend the main layout or create custom layout by adding them in the `layouts/` directory
|
||||
|
||||
## layouts/default.vue
|
||||
|
||||
You can extend the main layout by adding a `layouts/default.vue` file.
|
||||
|
||||
*Make sure to add the `<nuxt>` component when creating a layout to display the page component.*
|
||||
|
||||
The default layout source code is:
|
||||
```html
|
||||
<template>
|
||||
<nuxt/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## layouts/error.vue
|
||||
|
||||
You can customize the error page by adding a `layouts/error.vue` file.
|
||||
|
||||
This layout is special since your should not include `<nuxt/>` inside its template, see this layout as a component displayed when an error occurs (404, 500, etc).
|
||||
|
||||
The default error page source code is available on: https://github.com/nuxt/nuxt.js/blob/master/lib/app/components/nuxt-error.vue
|
||||
|
||||
## layouts/*.vue
|
||||
|
||||
See the [demonstration video](https://www.youtube.com/watch?v=YOKnSTp7d38).
|
||||
|
||||
Every file (*first level*) in the `layouts/` directory will create a custom layout accessible with the `layout` property in the page component.
|
||||
|
||||
*Make sure to add the `<nuxt>` component when creating a layout to display the page component.*
|
||||
|
||||
Example of `layouts/blog.vue`:
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<div>My blog navigation bar here</div>
|
||||
<nuxt/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
And then in `pages/posts.vue` I can tell Nuxt.js to use this custom layout:
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
layout: 'blog'
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Go to [http://localhost:3000](http://localhost:3000) and navigate trough the app. To see the custom error page: [http://localhost:3000/404](http://localhost:3000/404)
|
21
examples/custom-layouts/layouts/dark.vue
Normal file
21
examples/custom-layouts/layouts/dark.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="dark">
|
||||
<nuxt/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.dark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
background: black;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
.dark a {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<nuxt-container>
|
||||
<div>
|
||||
<img src="logo.png"/>
|
||||
<nuxt/>
|
||||
</nuxt-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
23
examples/custom-layouts/layouts/error.vue
Normal file
23
examples/custom-layouts/layouts/error.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Sorry, page not found</h1>
|
||||
<nuxt-link to="/">Home page</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['error']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
font-family: sans-serif;
|
||||
padding-top: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "nuxt-extend-app",
|
||||
"name": "nuxt-custom-layouts",
|
||||
"dependencies": {
|
||||
"nuxt": "latest"
|
||||
},
|
@ -7,9 +7,10 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'dark',
|
||||
data ({ req }) {
|
||||
return {
|
||||
name: req ? 'server' : 'client'
|
||||
name: req ? 'server' : 'client2'
|
||||
}
|
||||
}
|
||||
}
|
BIN
examples/custom-layouts/static/logo.png
Normal file
BIN
examples/custom-layouts/static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
@ -1,42 +0,0 @@
|
||||
# Extending the main app
|
||||
|
||||
> Nuxt.js allows you to extend the main application by adding a `layouts/app.vue` file
|
||||
|
||||
## The default app
|
||||
|
||||
The default source code of the main app is:
|
||||
```html
|
||||
<template>
|
||||
<nuxt-container>
|
||||
<nuxt/>
|
||||
</nuxt-container>
|
||||
</template>
|
||||
```
|
||||
|
||||
## The `layouts/app.vue` file
|
||||
|
||||
### 🎬 [Example video](https://www.youtube.com/watch?v=wBhia7uBxDA)
|
||||
|
||||
You have to make sure to add the `<nuxt-container>` and `<nuxt>` components when extending the app.
|
||||
|
||||
It is important that the code you add stays inside `<nuxt-container>`.
|
||||
|
||||
Example:
|
||||
```html
|
||||
<template>
|
||||
<nuxt-container>
|
||||
<div>My navigation bar here</div>
|
||||
<nuxt/>
|
||||
</nuxt-container>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Go to [http://localhost:3000](http://localhost:3000) and navigate trough the app. Notice how the logo at the top right stays between the pages, even on the error page: [http://localhost:3000/404](http://localhost:3000/404)
|
Binary file not shown.
Before Width: | Height: | Size: 2.0 KiB |
@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<p>Hi from {{ name }}</p>
|
||||
<div>
|
||||
<p>Hi from {{ name }}</p>
|
||||
<nuxt-link to="/">Home page</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -1,5 +1,53 @@
|
||||
<template>
|
||||
<nuxt-container>
|
||||
<nuxt/>
|
||||
</nuxt-container>
|
||||
<div id="__nuxt">
|
||||
<component v-if="layout" :is="layout"></component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let layouts = {
|
||||
<%
|
||||
var layoutsKeys = Object.keys(layouts);
|
||||
layoutsKeys.forEach(function (key, i) { %>
|
||||
_<%= key %>: process.BROWSER_BUILD ? () => System.import('<%= layouts[key] %>') : require('<%= layouts[key] %>')<%= (i + 1) < layoutsKeys.length ? ',' : '' %>
|
||||
<% }) %>
|
||||
}
|
||||
|
||||
export default {
|
||||
head: <%= JSON.stringify(head) %>,
|
||||
data: () => ({
|
||||
layout: null,
|
||||
layoutName: ''
|
||||
}),
|
||||
methods: {
|
||||
setLayout (layout) {
|
||||
if (!layout || !layouts['_' + layout]) layout = 'default'
|
||||
this.layoutName = layout
|
||||
let _layout = '_' + layout
|
||||
if (typeof layouts[_layout] === 'function') {
|
||||
return this.loadLayout(_layout)
|
||||
}
|
||||
this.layout = layouts[_layout]
|
||||
return Promise.resolve(this.layout)
|
||||
},
|
||||
loadLayout (_layout) {
|
||||
return layouts[_layout]()
|
||||
.then((Component) => {
|
||||
layouts[_layout] = Component
|
||||
this.layout = layouts[_layout]
|
||||
return this.layout
|
||||
})
|
||||
.catch((e) => {
|
||||
if (this.$nuxt) {
|
||||
return this.$nuxt.error({ statusCode: 500, message: e.message })
|
||||
}
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<% css.forEach(function (c) { %>
|
||||
<style src="<%= (typeof c === 'string' ? c : c.src) %>" lang="<%= (c.lang ? c.lang : 'css') %>"></style>
|
||||
<% }) %>
|
||||
|
@ -58,75 +58,88 @@ function loadAsyncComponents (to, ___, next) {
|
||||
function render (to, from, next) {
|
||||
let Components = getMatchedComponents(to)
|
||||
if (!Components.length) {
|
||||
this.error({ statusCode: 404, message: 'This page could not be found.', url: to.path })
|
||||
return next()
|
||||
// Default layout
|
||||
this.setLayout()
|
||||
.then(() => {
|
||||
this.error({ statusCode: 404, message: 'This page could not be found.', url: to.path })
|
||||
return next()
|
||||
})
|
||||
return
|
||||
}
|
||||
// Update ._data and other properties if hot reloaded
|
||||
Components.forEach(function (Component) {
|
||||
if (!Component._data) {
|
||||
Component._data = Component.options.data || noopData
|
||||
}
|
||||
if (Component._Ctor && Component._Ctor.options && Component._dataFn) {
|
||||
if (Component._Ctor && Component._Ctor.options) {
|
||||
Component.options.fetch = Component._Ctor.options.fetch
|
||||
const originalDataFn = Component._data.toString().replace(/\s/g, '')
|
||||
const dataFn = Component._dataFn
|
||||
const newDataFn = (Component._Ctor.options.data || noopData).toString().replace(/\s/g, '')
|
||||
// If component data method changed
|
||||
if (newDataFn !== originalDataFn && newDataFn !== dataFn) {
|
||||
Component._data = Component._Ctor.options.data || noopData
|
||||
if (Component._dataFn) {
|
||||
const originalDataFn = Component._data.toString().replace(/\s/g, '')
|
||||
const dataFn = Component._dataFn
|
||||
const newDataFn = (Component._Ctor.options.data || noopData).toString().replace(/\s/g, '')
|
||||
// If component data method changed
|
||||
if (newDataFn !== originalDataFn && newDataFn !== dataFn) {
|
||||
Component._data = Component._Ctor.options.data || noopData
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
this.setTransitions(mapTransitions(Components, to, from))
|
||||
this.error()
|
||||
let nextCalled = false
|
||||
let isValid = true
|
||||
Components.forEach((Component) => {
|
||||
if (!isValid) return
|
||||
if (typeof Component.options.validate !== 'function') return
|
||||
isValid = Component.options.validate({
|
||||
params: to.params || {},
|
||||
query: to.query || {}
|
||||
})
|
||||
})
|
||||
if (!isValid) {
|
||||
this.error({ statusCode: 404, message: 'This page could not be found.', url: to.path })
|
||||
return next()
|
||||
}
|
||||
Promise.all(Components.map((Component, i) => {
|
||||
// Check if only children route changed
|
||||
Component._path = compile(to.matched[i].path)(to.params)
|
||||
if (Component._path === _lastPaths[i] && (i + 1) !== Components.length) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
let promises = []
|
||||
const _next = function (path) {
|
||||
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
|
||||
nextCalled = true
|
||||
next(path)
|
||||
}
|
||||
const context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true, next: _next.bind(this), error: this.error.bind(this) })
|
||||
// Validate method
|
||||
if (Component._data && typeof Component._data === 'function') {
|
||||
var promise = promisify(Component._data, context)
|
||||
promise.then((data) => {
|
||||
Component.options.data = () => data || {}
|
||||
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')
|
||||
if (Component._Ctor && Component._Ctor.options) {
|
||||
Component._Ctor.options.data = Component.options.data
|
||||
}
|
||||
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
|
||||
// Set layout
|
||||
this.setLayout(Components[0].options.layout)
|
||||
.then(() => {
|
||||
// Pass validation?
|
||||
let isValid = true
|
||||
Components.forEach((Component) => {
|
||||
if (!isValid) return
|
||||
if (typeof Component.options.validate !== 'function') return
|
||||
isValid = Component.options.validate({
|
||||
params: to.params || {},
|
||||
query: to.query || {}
|
||||
})
|
||||
promises.push(promise)
|
||||
})
|
||||
if (!isValid) {
|
||||
this.error({ statusCode: 404, message: 'This page could not be found.', url: to.path })
|
||||
return next()
|
||||
}
|
||||
if (Component.options.fetch) {
|
||||
var p = Component.options.fetch(context)
|
||||
if (!(p instanceof Promise)) { p = Promise.resolve(p) }
|
||||
<%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
|
||||
promises.push(p)
|
||||
}
|
||||
return Promise.all(promises)
|
||||
}))
|
||||
return Promise.all(Components.map((Component, i) => {
|
||||
// Check if only children route changed
|
||||
Component._path = compile(to.matched[i].path)(to.params)
|
||||
if (Component._path === _lastPaths[i] && (i + 1) !== Components.length) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
let promises = []
|
||||
const _next = function (path) {
|
||||
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
|
||||
nextCalled = true
|
||||
next(path)
|
||||
}
|
||||
const context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true, next: _next.bind(this), error: this.error.bind(this) })
|
||||
// Validate method
|
||||
if (Component._data && typeof Component._data === 'function') {
|
||||
var promise = promisify(Component._data, context)
|
||||
promise.then((data) => {
|
||||
Component._cData = () => data || {}
|
||||
Component.options.data = Component._cData
|
||||
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')
|
||||
if (Component._Ctor && Component._Ctor.options) {
|
||||
Component._Ctor.options.data = Component.options.data
|
||||
}
|
||||
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
|
||||
})
|
||||
promises.push(promise)
|
||||
}
|
||||
if (Component.options.fetch) {
|
||||
var p = Component.options.fetch(context)
|
||||
if (!(p instanceof Promise)) { p = Promise.resolve(p) }
|
||||
<%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
|
||||
promises.push(p)
|
||||
}
|
||||
return Promise.all(promises)
|
||||
}))
|
||||
})
|
||||
.then(() => {
|
||||
_lastPaths = Components.map((Component, i) => compile(to.matched[i].path)(to.params))
|
||||
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
|
||||
@ -157,11 +170,13 @@ function fixPrepatch (to, ___) {
|
||||
}
|
||||
return instance.constructor.options.__file
|
||||
})
|
||||
hotReloadAPI(this)
|
||||
})
|
||||
}
|
||||
|
||||
// Special hot reload with data(context)
|
||||
function hotReloadAPI (_app) {
|
||||
if (!module.hot) return
|
||||
const $nuxt = _app.$nuxt
|
||||
var _forceUpdate = $nuxt.$forceUpdate.bind($nuxt)
|
||||
$nuxt.$forceUpdate = function () {
|
||||
@ -173,6 +188,14 @@ function hotReloadAPI (_app) {
|
||||
Component._Ctor = Component
|
||||
}
|
||||
let promises = []
|
||||
// If layout changed
|
||||
if (_app.layoutName !== Component.options.layout) {
|
||||
let promise = _app.setLayout(Component.options.layout)
|
||||
promise.then(() => {
|
||||
hotReloadAPI(_app)
|
||||
})
|
||||
promises.push(promise)
|
||||
}
|
||||
const next = function (path) {
|
||||
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
|
||||
router.push(path)
|
||||
@ -185,12 +208,16 @@ function hotReloadAPI (_app) {
|
||||
Component._data = Component._Ctor.options.data || noopData
|
||||
let p = promisify(Component._data, context)
|
||||
p.then((data) => {
|
||||
Component.options.data = () => data || {}
|
||||
Component._cData = () => data || {}
|
||||
Component.options.data = Component._cData
|
||||
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')
|
||||
Component._Ctor.options.data = Component.options.data
|
||||
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
|
||||
})
|
||||
promises.push(p)
|
||||
} else if (Component._cData) {
|
||||
Component.options.data = Component._cData
|
||||
Component._Ctor.options.data = Component.options.data
|
||||
}
|
||||
// Check if fetch has been updated
|
||||
const originalFetchFn = (Component.options.fetch || noopFetch).toString().replace(/\s/g, '')
|
||||
@ -233,7 +260,8 @@ const resolveComponents = flatMapComponents(router.match(path), (Component, _, m
|
||||
if (Component.options.data && typeof Component.options.data === 'function') {
|
||||
Component._data = Component.options.data
|
||||
if (NUXT.serverRendered) {
|
||||
Component.options.data = () => NUXT.data[index] || {}
|
||||
Component._cData = () => NUXT.data[index] || {}
|
||||
Component.options.data = Component._cData
|
||||
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')
|
||||
}
|
||||
if (Component._Ctor && Component._Ctor.options) {
|
||||
@ -264,6 +292,13 @@ function nuxtReady (app) {
|
||||
Promise.all(resolveComponents)
|
||||
.then((Components) => {
|
||||
const _app = new Vue(app)
|
||||
|
||||
return _app.setLayout(Components.length ? Components[0].options.layout : '')
|
||||
.then(() => {
|
||||
return { _app, Components }
|
||||
})
|
||||
})
|
||||
.then(({ _app, Components }) => {
|
||||
const mountApp = () => {
|
||||
_app.$mount('#__nuxt')
|
||||
<% if (loading) { %>
|
||||
@ -271,7 +306,7 @@ Promise.all(resolveComponents)
|
||||
_app.$loading = _app.$nuxt.$loading
|
||||
<% } %>
|
||||
// Hot reloading
|
||||
if (module.hot) hotReloadAPI(_app)
|
||||
hotReloadAPI(_app)
|
||||
// Call window.onNuxtReady callbacks
|
||||
Vue.nextTick(() => nuxtReady(_app))
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<div id="__nuxt">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'nuxt-container',
|
||||
head: <%= JSON.stringify(head) %>
|
||||
}
|
||||
</script>
|
||||
|
||||
<% css.forEach(function (c) { %>
|
||||
<style src="<%= (typeof c === 'string' ? c : c.src) %>" lang="<%= (c.lang ? c.lang : 'css') %>"></style>
|
||||
<% }) %>
|
@ -22,6 +22,8 @@ export default {
|
||||
Vue.prototype.$nuxt = this
|
||||
// Add this.$root.$nuxt
|
||||
this.$root.$nuxt = this
|
||||
// Bind $nuxt.setLayout(layout) to $root.setLayout
|
||||
this.setLayout = this.$root.setLayout.bind(this.$root)
|
||||
// add to window so we can listen when ready
|
||||
if (typeof window !== 'undefined') {
|
||||
window.$nuxt = this
|
||||
|
@ -4,14 +4,11 @@ import Vue from 'vue'
|
||||
import Meta from 'vue-meta'
|
||||
import router from './router.js'
|
||||
<% if (store) { %>import store from '~store/index.js'<% } %>
|
||||
import NuxtContainer from './components/nuxt-container.vue'
|
||||
import NuxtChild from './components/nuxt-child.js'
|
||||
import NuxtLink from './components/nuxt-link.js'
|
||||
import Nuxt from './components/nuxt.vue'
|
||||
import App from '<%= appPath %>'
|
||||
|
||||
// Component: <nuxt-container>
|
||||
Vue.component(NuxtContainer.name, NuxtContainer)
|
||||
// Component: <nuxt-child>
|
||||
Vue.component(NuxtChild.name, NuxtChild)
|
||||
// Component: <nuxt-link>
|
||||
|
3
lib/app/layouts/default.vue
Normal file
3
lib/app/layouts/default.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<nuxt/>
|
||||
</template>
|
@ -73,6 +73,10 @@ export default context => {
|
||||
}
|
||||
return Component
|
||||
})
|
||||
// Set layout
|
||||
return _app.setLayout(Components.length ? Components[0].options.layout : '')
|
||||
})
|
||||
.then(() => {
|
||||
// Call .validate()
|
||||
let isValid = true
|
||||
Components.forEach((Component) => {
|
||||
|
@ -132,6 +132,14 @@ function * buildFiles () {
|
||||
|
||||
function * generateRoutesAndFiles () {
|
||||
debug('Generating routes...')
|
||||
// Layouts
|
||||
let layouts = {}
|
||||
const layoutsFiles = yield glob('layouts/*.vue', { cwd: this.srcDir })
|
||||
layoutsFiles.forEach((file) => {
|
||||
let name = file.split('/').slice(-1)[0].replace('.vue', '')
|
||||
if (name === 'error') return
|
||||
layouts[name] = r(this.srcDir, file)
|
||||
})
|
||||
// Generate routes based on files
|
||||
const files = yield glob('pages/**/*.vue', { cwd: this.srcDir })
|
||||
this.routes = _.uniq(_.map(files, (file) => {
|
||||
@ -146,7 +154,6 @@ function * generateRoutesAndFiles () {
|
||||
'router.js',
|
||||
'server.js',
|
||||
'utils.js',
|
||||
'components/nuxt-container.vue',
|
||||
'components/nuxt-loading.vue',
|
||||
'components/nuxt-child.js',
|
||||
'components/nuxt-link.js',
|
||||
@ -165,6 +172,7 @@ function * generateRoutesAndFiles () {
|
||||
css: this.options.css,
|
||||
plugins: this.options.plugins.map((p) => r(this.srcDir, p)),
|
||||
appPath: './App.vue',
|
||||
layouts: layouts,
|
||||
loading: (typeof this.options.loading === 'string' ? r(this.srcDir, this.options.loading) : this.options.loading),
|
||||
transition: this.options.transition,
|
||||
components: {
|
||||
@ -174,12 +182,15 @@ function * generateRoutesAndFiles () {
|
||||
}
|
||||
// Format routes for the lib/app/router.js template
|
||||
templateVars.router.routes = createRoutes(files, this.srcDir)
|
||||
if (fs.existsSync(join(this.srcDir, 'layouts', 'app.vue'))) {
|
||||
templateVars.appPath = r(this.srcDir, 'layouts/app.vue')
|
||||
}
|
||||
if (fs.existsSync(join(this.srcDir, 'layouts', 'error.vue'))) {
|
||||
if (layoutsFiles.includes('layouts/error.vue')) {
|
||||
templateVars.components.ErrorPage = r(this.srcDir, 'layouts/error.vue')
|
||||
}
|
||||
// If no default layout, create its folder and add the default folder
|
||||
if (!layouts.default) {
|
||||
yield mkdirp(r(this.dir, '.nuxt/layouts'))
|
||||
templatesFiles.push('layouts/default.vue')
|
||||
layouts.default = r(__dirname, 'app', 'layouts', 'default.vue')
|
||||
}
|
||||
let moveTemplates = templatesFiles.map((file) => {
|
||||
return readFile(r(__dirname, 'app', file), 'utf8')
|
||||
.then((fileContent) => {
|
||||
@ -347,7 +358,14 @@ function createRenderer (bundle) {
|
||||
}
|
||||
|
||||
function watchPages () {
|
||||
const patterns = [ r(this.srcDir, 'pages/*.vue'), r(this.srcDir, 'pages/**/*.vue') ]
|
||||
const patterns = [
|
||||
r(this.srcDir, 'pages'),
|
||||
r(this.srcDir, 'pages/*.vue'),
|
||||
r(this.srcDir, 'pages/**/*.vue'),
|
||||
r(this.srcDir, 'layouts'),
|
||||
r(this.srcDir, 'layouts/*.vue'),
|
||||
r(this.srcDir, 'layouts/**/*.vue')
|
||||
]
|
||||
const options = {
|
||||
ignoreInitial: true
|
||||
}
|
||||
|
16
package.json
16
package.json
@ -45,7 +45,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-html": "^0.0.6",
|
||||
"autoprefixer": "^6.5.4",
|
||||
"autoprefixer": "^6.6.0",
|
||||
"babel-core": "^6.21.0",
|
||||
"babel-loader": "^6.2.10",
|
||||
"babel-polyfill": "^6.20.0",
|
||||
@ -55,7 +55,7 @@
|
||||
"co": "^4.6.0",
|
||||
"cross-spawn": "^5.0.1",
|
||||
"css-loader": "^0.26.1",
|
||||
"debug": "^2.4.5",
|
||||
"debug": "^2.5.1",
|
||||
"es6-object-assign": "^1.0.3",
|
||||
"es6-promise": "^4.0.5",
|
||||
"extract-text-webpack-plugin": "2.0.0-beta.4",
|
||||
@ -64,7 +64,7 @@
|
||||
"glob": "^7.1.1",
|
||||
"hash-sum": "^1.0.2",
|
||||
"html-minifier": "^3.2.3",
|
||||
"lodash": "^4.17.2",
|
||||
"lodash": "^4.17.3",
|
||||
"lru-cache": "^4.0.2",
|
||||
"memory-fs": "^0.4.1",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
@ -72,16 +72,16 @@
|
||||
"serialize-javascript": "^1.3.0",
|
||||
"serve-static": "^1.11.1",
|
||||
"url-loader": "^0.5.7",
|
||||
"vue": "^2.1.6",
|
||||
"vue": "^2.1.7",
|
||||
"vue-loader": "^10.0.2",
|
||||
"vue-meta": "^0.5.3",
|
||||
"vue-router": "^2.1.1",
|
||||
"vue-server-renderer": "^2.1.6",
|
||||
"vue-template-compiler": "^2.1.6",
|
||||
"vue-server-renderer": "^2.1.7",
|
||||
"vue-template-compiler": "^2.1.7",
|
||||
"vuex": "^2.1.1",
|
||||
"webpack": "2.2.0-rc.1",
|
||||
"webpack": "2.2.0-rc.2",
|
||||
"webpack-dev-middleware": "^1.9.0",
|
||||
"webpack-hot-middleware": "^2.13.2"
|
||||
"webpack-hot-middleware": "^2.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^0.17.0",
|
||||
|
51
test/dynamic-routes.test.js
Normal file
51
test/dynamic-routes.test.js
Normal file
@ -0,0 +1,51 @@
|
||||
import test from 'ava'
|
||||
import { resolve } from 'path'
|
||||
import fs from 'fs'
|
||||
import pify from 'pify'
|
||||
const readFile = pify(fs.readFile)
|
||||
|
||||
// Init nuxt.js and create server listening on localhost:4000
|
||||
test.before('Init Nuxt.js', async t => {
|
||||
const Nuxt = require('../')
|
||||
const nuxt = new Nuxt({
|
||||
rootDir: resolve(__dirname, 'fixtures/dynamic-routes'),
|
||||
dev: false
|
||||
})
|
||||
await nuxt.build()
|
||||
})
|
||||
|
||||
test('Check .nuxt/router.js', t => {
|
||||
return readFile(resolve(__dirname, './fixtures/dynamic-routes/.nuxt/router.js'), 'utf-8')
|
||||
.then((routerFile) => {
|
||||
routerFile = routerFile.slice(
|
||||
routerFile.indexOf('routes: ['),
|
||||
-3
|
||||
)
|
||||
.replace('routes: [', '[')
|
||||
.replace(/ _[0-9A-z]+,/g, ' "",')
|
||||
let routes = eval('( ' + routerFile + ')') // eslint-disable-line no-eval
|
||||
// pages/test/index.vue
|
||||
t.is(routes[0].path, '/test')
|
||||
t.is(routes[0].name, 'test')
|
||||
// pages/parent.vue
|
||||
t.is(routes[1].path, '/parent')
|
||||
t.falsy(routes[1].name) // parent route has no name
|
||||
// pages/parent/*.vue
|
||||
t.is(routes[1].children.length, 3) // parent has 3 children
|
||||
t.deepEqual(routes[1].children.map((r) => r.path), ['', 'teub', 'child'])
|
||||
t.deepEqual(routes[1].children.map((r) => r.name), ['parent', 'parent-teub', 'parent-child'])
|
||||
// pages/test/users.vue
|
||||
t.is(routes[2].path, '/test/users')
|
||||
t.falsy(routes[2].name) // parent route has no name
|
||||
// pages/test/users/*.vue
|
||||
t.is(routes[2].children.length, 3) // parent has 3 children
|
||||
t.deepEqual(routes[2].children.map((r) => r.path), ['', ':id', ':index/teub'])
|
||||
t.deepEqual(routes[2].children.map((r) => r.name), ['test-users', 'test-users-id', 'test-users-index-teub'])
|
||||
// pages/_slug.vue
|
||||
t.is(routes[3].path, '/:slug?')
|
||||
t.is(routes[3].name, 'slug')
|
||||
// pages/_key/_id.vue
|
||||
t.is(routes[4].path, '/:key?/:id?')
|
||||
t.is(routes[4].name, 'key-id')
|
||||
})
|
||||
})
|
185
test/fixtures/dynamic-routes/README.md
vendored
185
test/fixtures/dynamic-routes/README.md
vendored
@ -1,185 +0,0 @@
|
||||
# Defining custom routes with Nuxt.js
|
||||
|
||||
> Nuxt.js is based on `vue-router` and let you to defined custom routes easily :rocket:
|
||||
|
||||
## Concept
|
||||
|
||||
Nuxt.js generates automatically the `vue-router` configuration according to your file tree of `.vue` files inside the `pages/` directory.
|
||||
|
||||
## Basic routes
|
||||
|
||||
This file tree:
|
||||
|
||||
```bash
|
||||
pages/
|
||||
--| team/
|
||||
-----| index.vue
|
||||
-----| about.vue
|
||||
--| index.vue
|
||||
```
|
||||
|
||||
will automatically generate:
|
||||
|
||||
```js
|
||||
router: {
|
||||
routes: [
|
||||
{
|
||||
name: 'index',
|
||||
path: '/',
|
||||
component: 'pages/index.vue'
|
||||
},
|
||||
{
|
||||
name: 'team',
|
||||
path: '/team',
|
||||
component: 'pages/team/index.vue'
|
||||
},
|
||||
{
|
||||
name: 'team-about',
|
||||
path: '/team/about',
|
||||
component: 'pages/team/about.vue'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic routes
|
||||
|
||||
To define a dynamic route with a param, you need to define a `.vue` file **prefixed by an underscore**.
|
||||
|
||||
This file tree:
|
||||
|
||||
```bash
|
||||
pages/
|
||||
--| users/
|
||||
-----| _id.vue
|
||||
-----| index.vue
|
||||
```
|
||||
|
||||
will automatically generate:
|
||||
|
||||
```js
|
||||
router: {
|
||||
routes: [
|
||||
{
|
||||
name: 'users',
|
||||
path: '/users',
|
||||
component: 'pages/users/index.vue'
|
||||
},
|
||||
{
|
||||
name: 'users-id',
|
||||
path: '/users/:id',
|
||||
component: 'pages/users/_id.vue'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Additional feature: validate (optional)
|
||||
|
||||
Nuxt.js lets you define a validator function inside your dynamic route component (In this example: `pages/users/_id.vue`).
|
||||
|
||||
If the validate method does not return `true`, Nuxt.js will automatically load the 404 error page.
|
||||
|
||||
```js
|
||||
<script>
|
||||
export default {
|
||||
validate ({ params }) {
|
||||
return /^\d+$/.test(params.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Nested Routes (children)
|
||||
|
||||
To define a nested route, you need to create a `.vue` file with the **same name as the directory** which contain your children views.
|
||||
> Don't forget to put `<nuxt-child></nuxt-child>` inside your parent `.vue` file.
|
||||
|
||||
This file tree:
|
||||
|
||||
```bash
|
||||
pages/
|
||||
--| users/
|
||||
-----| _id.vue
|
||||
--| users.vue
|
||||
```
|
||||
|
||||
will automatically generate:
|
||||
|
||||
```js
|
||||
router: {
|
||||
routes: [
|
||||
{
|
||||
path: '/users',
|
||||
component: 'pages/users.vue',
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: 'pages/users/_id.vue',
|
||||
name: 'users-id'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Nested Routes
|
||||
|
||||
This file tree:
|
||||
|
||||
```bash
|
||||
pages/
|
||||
--| posts/
|
||||
-----| _slug/
|
||||
--------| _name.vue
|
||||
--------| comments.vue
|
||||
-----| _slug.vue
|
||||
-----| index.vue
|
||||
--| posts.vue
|
||||
```
|
||||
|
||||
will automatically generate:
|
||||
|
||||
```js
|
||||
router: {
|
||||
routes: [
|
||||
{
|
||||
path: '/posts',
|
||||
component: 'pages/posts.vue',
|
||||
children: [
|
||||
{
|
||||
path '',
|
||||
component: 'pages/posts/index.vue',
|
||||
name: 'posts'
|
||||
},
|
||||
{
|
||||
path: ':slug',
|
||||
component: 'pages/posts/_slug.vue',
|
||||
children: [
|
||||
{
|
||||
path: 'comments',
|
||||
component: 'pages/posts/_slug/comments.vue',
|
||||
name: 'posts-slug-comments'
|
||||
},
|
||||
{
|
||||
path: ':name',
|
||||
component: 'pages/posts/_slug/_name.vue',
|
||||
name: 'posts-slug-name'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Go to [http://localhost:3000](http://localhost:3000) and navigate through the pages.
|
5
test/fixtures/dynamic-routes/nuxt.config.js
vendored
5
test/fixtures/dynamic-routes/nuxt.config.js
vendored
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
build: {
|
||||
vendor: ['axios']
|
||||
}
|
||||
}
|
13
test/fixtures/dynamic-routes/package.json
vendored
13
test/fixtures/dynamic-routes/package.json
vendored
@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "nuxt-custom-routes",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^0.15.2",
|
||||
"nuxt": "latest"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start"
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h2>Users</h2>
|
||||
<ul class="users">
|
||||
<li v-for="user in users">
|
||||
<nuxt-link :to="'/users/'+user.id">{{ user.name }}</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return axios.get('https://jsonplaceholder.typicode.com/users')
|
||||
.then((res) => {
|
||||
return { users: res.data }
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
text-align: center;
|
||||
margin-top: 100px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.users {
|
||||
list-style-type: none;
|
||||
}
|
||||
.users li a {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
border: 1px #ddd solid;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
color: #222;
|
||||
text-decoration: none;
|
||||
}
|
||||
.users li a:hover {
|
||||
color: orange;
|
||||
}
|
||||
</style>
|
@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<div class="user">
|
||||
<h3>{{ name }}</h3>
|
||||
<h4>@{{ username }}</h4>
|
||||
<p>Email : {{ email }}</p>
|
||||
<p><nuxt-link to="/">List of users</nuxt-link></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
validate ({ params }) {
|
||||
return !isNaN(+params.id)
|
||||
},
|
||||
data ({ params, error }) {
|
||||
return axios.get(`https://jsonplaceholder.typicode.com/users/${+params.id}`)
|
||||
.then((res) => res.data)
|
||||
.catch(() => {
|
||||
error({ message: 'User not found', statusCode: 404 })
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user {
|
||||
text-align: center;
|
||||
margin-top: 100px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
</style>
|
5
test/fixtures/with-config/layouts/app.vue
vendored
5
test/fixtures/with-config/layouts/app.vue
vendored
@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<nuxt-container>
|
||||
<nuxt/>
|
||||
</nuxt-container>
|
||||
</template>
|
6
test/fixtures/with-config/layouts/custom.vue
vendored
Normal file
6
test/fixtures/with-config/layouts/custom.vue
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Custom layout</h1>
|
||||
<nuxt/>
|
||||
</div>
|
||||
</template>
|
6
test/fixtures/with-config/layouts/default.vue
vendored
Normal file
6
test/fixtures/with-config/layouts/default.vue
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Default layout</h1>
|
||||
<nuxt/>
|
||||
</div>
|
||||
</template>
|
6
test/fixtures/with-config/pages/about.vue
vendored
6
test/fixtures/with-config/pages/about.vue
vendored
@ -4,3 +4,9 @@
|
||||
<nuxt-link to="/">Home page</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'custom'
|
||||
}
|
||||
</script>
|
||||
|
@ -27,9 +27,17 @@ test('/', async t => {
|
||||
test('/test/ (router base)', async t => {
|
||||
const window = await nuxt.renderAndGetWindow(url('/test/'))
|
||||
const html = window.document.body.innerHTML
|
||||
t.true(html.includes('<h1>Default layout</h1>'))
|
||||
t.true(html.includes('<h1>I have custom configurations</h1>'))
|
||||
})
|
||||
|
||||
test('/test/about (custom layout)', async t => {
|
||||
const window = await nuxt.renderAndGetWindow(url('/test/about'))
|
||||
const html = window.document.body.innerHTML
|
||||
t.true(html.includes('<h1>Custom layout</h1>'))
|
||||
t.true(html.includes('<h1>About page</h1>'))
|
||||
})
|
||||
|
||||
test('/test/env', async t => {
|
||||
const window = await nuxt.renderAndGetWindow(url('/test/env'))
|
||||
const html = window.document.body.innerHTML
|
||||
|
Loading…
Reference in New Issue
Block a user