Merge pull request #77 from nuxt/feature-layout

Layouts feature + bug fixes
This commit is contained in:
Sébastien Chopin 2016-12-24 18:59:01 +01:00 committed by GitHub
commit 593e12f348
31 changed files with 374 additions and 427 deletions

View 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)

View 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>

View File

@ -1,8 +1,8 @@
<template>
<nuxt-container>
<div>
<img src="logo.png"/>
<nuxt/>
</nuxt-container>
</div>
</template>
<style scoped>

View 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>

View File

@ -1,5 +1,5 @@
{
"name": "nuxt-extend-app",
"name": "nuxt-custom-layouts",
"dependencies": {
"nuxt": "latest"
},

View File

@ -7,9 +7,10 @@
<script>
export default {
layout: 'dark',
data ({ req }) {
return {
name: req ? 'server' : 'client'
name: req ? 'server' : 'client2'
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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

View File

@ -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>

View File

@ -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>
<% }) %>

View File

@ -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))
}

View File

@ -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>
<% }) %>

View File

@ -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

View File

@ -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>

View File

@ -0,0 +1,3 @@
<template>
<nuxt/>
</template>

View File

@ -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) => {

View File

@ -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
}

View File

@ -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",

View 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')
})
})

View File

@ -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.

View File

@ -1,5 +0,0 @@
module.exports = {
build: {
vendor: ['axios']
}
}

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +0,0 @@
<template>
<nuxt-container>
<nuxt/>
</nuxt-container>
</template>

View File

@ -0,0 +1,6 @@
<template>
<div>
<h1>Custom layout</h1>
<nuxt/>
</div>
</template>

View File

@ -0,0 +1,6 @@
<template>
<div>
<h1>Default layout</h1>
<nuxt/>
</div>
</template>

View File

@ -4,3 +4,9 @@
<nuxt-link to="/">Home page</nuxt-link>
</div>
</template>
<script>
export default {
layout: 'custom'
}
</script>

View File

@ -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