diff --git a/examples/with-next-layouts/layouts/AppLayout.js b/examples/with-next-layouts/layouts/AppLayout.js new file mode 100644 index 00000000000000..e2d1c32a43827a --- /dev/null +++ b/examples/with-next-layouts/layouts/AppLayout.js @@ -0,0 +1,19 @@ +import React from 'react' +import delay from '../modules/delay' + +export default class AppLayout extends React.Component { + static async getInitialProps () { + return { + delay: await delay(1000) + } + } + + render () { + return ( +
+

App header

+ {this.props.children} +
+ ) + } +} diff --git a/examples/with-next-layouts/layouts/ContentLayout.js b/examples/with-next-layouts/layouts/ContentLayout.js new file mode 100644 index 00000000000000..346112c50f5bca --- /dev/null +++ b/examples/with-next-layouts/layouts/ContentLayout.js @@ -0,0 +1,22 @@ +import React from 'react' +import delay from '../modules/delay' +import AppLayout from './AppLayout' + +export default class ContentLayout extends React.Component { + static parentLayout = AppLayout + static async getInitialProps () { + return { + delay: await delay(2000) + } + } + + render () { + return ( +
+
+ {this.props.children} +
+
+ ) + } +} diff --git a/examples/with-next-layouts/modules/delay.js b/examples/with-next-layouts/modules/delay.js new file mode 100644 index 00000000000000..27a13789aa79d3 --- /dev/null +++ b/examples/with-next-layouts/modules/delay.js @@ -0,0 +1,6 @@ +export default function delay (ms) { + return new Promise(resolve => setTimeout(() => { + console.log(`sleep ${ms} ms.`) + resolve(ms) + }, ms)) +} diff --git a/examples/with-next-layouts/modules/next-layouts.js b/examples/with-next-layouts/modules/next-layouts.js new file mode 100644 index 00000000000000..0730dce72e642b --- /dev/null +++ b/examples/with-next-layouts/modules/next-layouts.js @@ -0,0 +1,81 @@ +import React from 'react' +const defaultOptions = { + getInitialPropsMode: 'sequential' +} + +export default function applyLayout (PageComponent, options) { + options = Object.assign({}, defaultOptions, options) + + if (!isFunction(PageComponent.layout)) { + warn('Missing static property "layout" of page component.') + return PageComponent + } + + if (isFunction(PageComponent.renderPage)) { + warn('You should not define static property "renderPage" of page component when the layout is applying') + return PageComponent + } + + // collect layouts & getInitialProps + let layouts = [PageComponent.layout] + while (isFunction(layouts[0].parentLayout)) { + layouts.unshift(layouts[0].parentLayout) + } + const layoutGetInitialPropsList = layouts.map(Layout => isFunction(Layout.getInitialProps) ? Layout.getInitialProps : () => ({})) + const pageGetInitialProps = isFunction(PageComponent.getInitialProps) ? PageComponent.getInitialProps : () => ({}) + + PageComponent.getInitialProps = async(ctx) => { + let layoutPropsList = [] + let pageProps + + if (options.getInitialPropsMode === 'sequential') { + for (const layoutGetInitialProps of layoutGetInitialPropsList) { + const layoutProps = await layoutGetInitialProps(ctx) + layoutPropsList.push(layoutProps) + } + pageProps = await pageGetInitialProps(ctx) + } else if (options.getInitialPropsMode === 'concurrent') { + const promises = layoutGetInitialPropsList.map(layoutGetInitialProps => layoutGetInitialProps(ctx)) + promises.push(pageGetInitialProps(ctx)) + const promiseResults = await Promise.all(promises) + pageProps = promiseResults.pop() + layoutPropsList = promiseResults + } + + return { layoutPropsList, pageProps } + } + + PageComponent.renderPage = ({ Component, props: { layoutPropsList, pageProps }, url }) => { + return renderLayout({ Component, layouts, pageProps, layoutPropsList, url }) + } + + return PageComponent +} + +function renderLayout ({ Component, layouts, pageProps, layoutPropsList, url }) { + const Layout = layouts[0] + const layoutProps = layoutPropsList[0] + return (Layout) ? ( + + {renderLayout({ + Component, + layouts: layouts.slice(1, layouts.length), + pageProps, + layoutPropsList: layoutPropsList.slice(1, layoutPropsList.length), + url + })} + + ) : ( + + ) +} + +function isFunction (func) { + return typeof func === 'function' +} + +function warn (message) { + if (process.env.NODE_ENV !== 'production') { + console.error(message) + } +} diff --git a/examples/with-next-layouts/package.json b/examples/with-next-layouts/package.json new file mode 100644 index 00000000000000..0fdb3b36f4bff2 --- /dev/null +++ b/examples/with-next-layouts/package.json @@ -0,0 +1,15 @@ +{ + "name": "with-next-layouts", + "version": "1.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "react": "^16.0.0", + "react-dom": "^16.0.0" + }, + "license": "ISC" +} diff --git a/examples/with-next-layouts/pages/about.js b/examples/with-next-layouts/pages/about.js new file mode 100644 index 00000000000000..bd6af1ed86eef1 --- /dev/null +++ b/examples/with-next-layouts/pages/about.js @@ -0,0 +1,23 @@ +import React from 'react' +import delay from '../modules/delay' +import applyLayout from '../modules/next-layouts' +import ContentLayout from '../components/ContentLayout' + +class AboutPage extends React.Component { + static layout = ContentLayout + static async getInitialProps () { + return { + delay: await delay(3000) + } + } + + render () { + return ( +

about

+ ) + } +} + +export default applyLayout(AboutPage, { + getInitialPropsMode: 'concurrent' +}) diff --git a/examples/with-next-layouts/pages/index.js b/examples/with-next-layouts/pages/index.js new file mode 100644 index 00000000000000..b1d7e84a2ef6f0 --- /dev/null +++ b/examples/with-next-layouts/pages/index.js @@ -0,0 +1,27 @@ +import React from 'react' +import Link from 'next/link' +import delay from '../modules/delay' +import applyLayout from '../modules/next-layouts' +import ContentLayout from '../components/ContentLayout' + +class IndexPage extends React.Component { + static layout = ContentLayout + static async getInitialProps () { + return { + delay: await delay(3000) + } + } + + render () { + return ( +
+ index + + about + +
+ ) + } +} + +export default applyLayout(IndexPage) diff --git a/next-layouts.js b/next-layouts.js new file mode 100644 index 00000000000000..10da704670e37f --- /dev/null +++ b/next-layouts.js @@ -0,0 +1,80 @@ +import React from 'react' +const defaultOptions = { + getInitialPropsMode: 'sequential' +} + +export default function applyLayout (PageComponent, options) { + options = Object.assign({}, defaultOptions, options) + + if (!isFunction(PageComponent.layout)) { + warn('Missing static property "layout" of page component.') + return PageComponent + } + + if (isFunction(PageComponent.renderPage)) { + warn('You should not define static property "renderPage" of page component when the layout is applying') + } + + // collect layouts & getInitialProps + let layouts = [PageComponent.layout] + while (isFunction(layouts[0].parentLayout)) { + layouts.unshift(layouts[0].parentLayout) + } + const layoutGetInitialPropsList = layouts.map(Layout => isFunction(Layout.getInitialProps) ? Layout.getInitialProps : () => ({})) + const pageGetInitialProps = isFunction(PageComponent.getInitialProps) ? PageComponent.getInitialProps : () => ({}) + + PageComponent.getInitialProps = async(ctx) => { + let layoutPropsList = [] + let pageProps + + if (options.getInitialPropsMode === 'sequential') { + for (const layoutGetInitialProps of layoutGetInitialPropsList) { + const layoutProps = await layoutGetInitialProps(ctx) + layoutPropsList.push(layoutProps) + } + pageProps = await pageGetInitialProps(ctx) + } else if (options.getInitialPropsMode === 'concurrent') { + const promises = layoutGetInitialPropsList.map(layoutGetInitialProps => layoutGetInitialProps(ctx)) + promises.push(pageGetInitialProps(ctx)) + const promiseResults = await Promise.all(promises) + pageProps = promiseResults.pop() + layoutPropsList = promiseResults + } + + return { layoutPropsList, pageProps } + } + + PageComponent.renderPage = ({ Component, props: { layoutPropsList, pageProps }, url }) => { + return renderLayout({ Component, layouts, pageProps, layoutPropsList, url }) + } + + return PageComponent +} + +function renderLayout ({ Component, layouts, pageProps, layoutPropsList, url }) { + const Layout = layouts[0] + const layoutProps = layoutPropsList[0] + return (Layout) ? ( + + {renderLayout({ + Component, + layouts: layouts.slice(1, layouts.length), + pageProps, + layoutPropsList: layoutPropsList.slice(1, layoutPropsList.length), + url + })} + + ) : ( + + ) +} + +function isFunction (func) { + return typeof func === 'function' +} + +function warn (message) { + if (process.env.NODE_ENV !== 'production') { + console.error(message) + } +}