Skip to content

Commit

Permalink
feat: add support for nested layouts
Browse files Browse the repository at this point in the history
BREAKING CHANGE: the API has greatly changed, please check the README
  • Loading branch information
satazor committed Feb 27, 2020
1 parent c55db34 commit 3ac0e7a
Show file tree
Hide file tree
Showing 18 changed files with 1,801 additions and 607 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": [["@moxy/babel-preset/lib", { "react": true }]]
}
156 changes: 96 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
[david-dm-dev-url]:https://david-dm.org/moxystudio/next-layout?type=dev
[david-dm-dev-image]:https://img.shields.io/david/dev/moxystudio/next-layout.svg

Add persistent layouts to your Next.js projects in a declarative way.
Add persistent and nested layouts to your Next.js projects in a declarative way.

## Installation

Expand All @@ -26,22 +26,24 @@ This library is written in modern JavaScript and is published in both CommonJS a

## Motivation

Next.js projects usually have the need to have one or more layouts. Layouts are the "shell" of your app and usually contain navigation elements, such as an header and a footer. In the ideal scenario, each page would be able to say which layout they want to use, including tweaking its properties dynamically, such as `variant="light"`. However, we also want to keep the layout persistent in the React tree, to avoid having to remount it every time a user navigate between pages.
Next.js projects usually have the need to have one or more layouts. Layouts are the "shell" of your app and usually contain navigation elements, such as an header and a footer. In more complex projects, you might also need to have nested layouts which are often associated with nested routes.

In the ideal scenario, each page would be able to say which layout they want to use, including tweaking its properties dynamically, such as `variant="light"`. However, we also want to keep the layout persistent in the React tree, to avoid having to remount it every time a user navigate between pages.

Historically, projects overlook the need of multiple layouts or the ability to change layout props between pages. They start off with a simple layout and only later they handle this need, often with poor and non-scalable solutions.

This library solves the need for multi-layouts and changing layout props dynamically in a consistent and reusable way.

## Usage

Setup `<LayoutManager>` in your `pages/_app.js` component:
Setup `<LayoutTree>` in your `pages/_app.js` component:

```js
import React from 'react';
import { LayoutManager } from '@moxy/next-layout';
import { LayoutTree } from '@moxy/next-layout';

const App = ({ Component, pageProps }) => (
<LayoutManager
<LayoutTree
Component={ Component }
pageProps={ pageProps } />
);
Expand All @@ -66,15 +68,65 @@ const About = () => (
export default withLayout(<PrimaryLayout variant="light" />)(About);
```

ℹ️ Layouts will receive the page to be rendered as the `children` prop.
ℹ️ The `PrimaryLayout` component will receive the page to be rendered as the `children` prop.

### Nested layouts

Nested layouts are as easy as nesting them in the `withLayout`. Let's say that you have two account pages, `pages/account/profile.js` and `pages/account/settings`, and you want them to be wrapped by an `AccountLayout`. You would define the pages like so:

```js
// pages/account/profile
import React from 'react';
import { withLayout } from '@moxy/next-layout';
import { PrimaryLayout, AccountLayout } from '../components';
import styles from './.module.css';

const AccountProfile = () => (
<div className={ styles.accountProfile }>
<h1>Account Profile</h1>
</div>
);

export default withLayout(
<PrimaryLayout>
<AccountLayout />
<PrimaryLayout />
)(AccountProfile);
```

```js
// pages/account/settings
import React from 'react';
import { withLayout } from '@moxy/next-layout';
import { PrimaryLayout, AccountLayout } from '../components';
import styles from './.module.css';

const AccountSettings = () => (
<div className={ styles.accountSettings }>
<h1>Account Settings</h1>
</div>
);

export default withLayout(
<PrimaryLayout>
<AccountLayout />
<PrimaryLayout />
)(AccountSettings);
```

ℹ️ The `PrimaryLayout` component will receive `AccountLayout` as a children, which in turn will receive the page as children too.

ℹ️ You could create a `withAccountLayout` HOC to avoid repeating the layout tree in every account page.

⚠️ The layout tree specified in `withLayout` must be a unary tree, that is, a tree where nodes just have one child.

## API

`@moxy/next-layout` exposes a `<LayoutManager>` component and a `withLayout` to be used in pages.
`@moxy/next-layout` exposes a `<LayoutTree>` component and a `withLayout` HOC to be used in pages.

### &lt;LayoutManager&gt;
### &lt;LayoutTree&gt;

A component that manages the current layout to be used based on what the active page specifies. It keeps the layout persistent between page transitions whenever possible (e.g.: when the layout is the same).
A component that infers the layout tree based on what the active page specifies. It keeps the layout persistent between page transitions whenever possible (e.g.: when the layout is the same).

Here's the list of props it supports:

Expand All @@ -99,11 +151,11 @@ The default layout to be used when a child page doesn't explicitly sets one.
```js
// pages/_app.js
import React from 'react';
import { LayoutManager } from '@moxy/next-layout';
import { LayoutTree } from '@moxy/next-layout';
import { PrimaryLayout } from '../components';

const App = ({ Component, pageProps }) => (
<LayoutManager
<LayoutTree
Component={ Component }
pageProps={ pageProps }
defaultLayout={ <PrimaryLayout /> } />
Expand All @@ -116,73 +168,55 @@ export default App;

Type: `function`

A [render prop](https://reactjs.org/docs/render-props.html) to override the default render behavior.
A [render prop](https://reactjs.org/docs/render-props.html) to override the default render behavior, which just regularly renders the tree.

Its signature is `({ Layout, layoutProps, layoutKey, Component, pageProps, pageKey }) => <ReactElement>`, where:
Its signature is `(tree) => <ReactElement>`, where: `tree` is the React's tree composed by layout elements and a leaf page element.

- `Layout` is the layout React component that should be rendered
- `layoutProps` is the props that should be passed to the layout React component
- `layoutKey` is a unique string for the layout to be used as `key`
- `Component` is the page React component that should be rendered
- `pageProps` is the props that should be passed to the page React component, and already includes `setLayoutProps` if the page was wrapped with [`withLayout`](#withlayoutlayoutpage)
- `pageKey` is a unique string for the page to be used as `key`
This might be useful if you want to add animations between page transitions.

Passing a custom `children` render prop is useful to add layout and page transitions. Here's an example that uses [`react-transition-group`](https://reactcommunity.org/react-transition-group/) to apply a simple fade transition between layouts and pages:
### withLayout(mapLayoutStateToLayoutTree?, initialLayoutState?)(Page)

```js
// pages/_app.js
import React from 'react';
import { LayoutManager } from '@moxy/next-layout';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { PrimaryLayout } from '../components';
Sets up a `Page` component with the ability to specify which layout tree to use. Moreover, it injects a `setLayoutState` prop so that you may dynamically update the layout tree.

const App = ({ Component, pageProps }) => (
<LayoutManager
Component={ Component }
pageProps={ pageProps }
defaultLayout={ <PrimaryLayout /> }>
{ ({ Layout, layoutProps, layoutKey, Component, pageProps, pageKey }) => (
<TransitionGroup>
<CSSTransition key={ layoutKey } classNames="fade">
<Layout { ...layoutProps }>
<TransitionGroup>
<CSSTransition key={ pageKey } classNames="fade">
<Component { ...pageProps } />
</CSSTransition>
</TransitionGroup>
</Layout>
</CSSTransition>
</TransitionGroup>
) }
</LayoutManager>
);
#### mapLayoutStateToLayoutTree

Type: `ReactElement` or `function`

In simple cases, you may defined a "static" layout tree, like so:

```js
export default withLayout(<PrimaryLayout variant="light" />)(Home);
```

### withLayout(layout?)(Page)
However, you might be having external props, component state or other mutations influencing the layout tree. In those cases, you may pass a function that maps **layout state** into a tree, with the following signature: `(layoutState) => <ReactElement>`. Here's an example:

```js
const mapLayoutStateToLayoutTree = ({ variant }) => <PrimaryLayout variant={ variant } />;

Sets up a `Page` component with the ability to select which `layout` to use. Moreover, it injects a `setLayoutProps` prop so that you may dynamically update the layout props.
export default withLayout(mapLayoutStateToLayoutTree, { variant: 'light' })(Home);
```

#### layout
The function is run initially and every time the *layout state* changes.

Type: `ReactElement` or `function`
#### initialLayoutState

The layout to use for the `Page`. Can either be a `ReactElement` or a function that returns it.
Type: `object` or `function`

The function form is useful when page props affects layout props. It has the following signature: `(ownProps) => <ReactElement>`. Please note that the function only runs once to determine the layout and its initial props.
The initial **layout state** to be passed to `mapLayoutStateToLayoutTree`. If your initial *layout state* depends on the props you receive, you may pass a function with the following signature: `(props) => <object>`.

#### Page

Type: `ReactElementType`

The page component to wrap.

#### Injected setLayoutProps
#### Injected setLayoutState

Type: `function`

Allows to dynamically change the layout props. Has the following signature: `(updater | stateChange, callback?)`.
Allows to dynamically change the layout state. Has the following signature: `(newState | updater?)`.

The behavior of `setLayoutProps` is exactly the same as [`setState`](https://reactjs.org/docs/react-component.html#setstate) of class components, supporting both an object or an updater function.
The behavior of `setLayoutState` is exactly the same as [`setState`](https://reactjs.org/docs/react-component.html#setstate) of class components: it merges properties and it supports both an object or an updater function.

```js
// pages/about.js
Expand All @@ -192,11 +226,11 @@ import { PrimaryLayout } from '../components';

import styles from './about.module.css';

const About = ({ setLayoutProps }) => {
const About = ({ setLayoutState }) => {
const handleSetToDark = useCallback(() => {
setLayoutProps({ variant="dark" });
// ..or setLayoutProps((layoutProps) => ({ variant="dark" }));
}, [setLayoutProps]);
setLayoutState({ variant="dark" });
// ..or setLayoutState((layoutState) => ({ variant="dark" }));
}, [setLayoutState]);

return (
<div className={ styles.about }>
Expand All @@ -206,7 +240,9 @@ const About = ({ setLayoutProps }) => {
);
};

export default withLayout(<PrimaryLayout variant="light" />)(About);
const mapLayoutStateToLayoutTree = ({ variant }) => <PrimaryLayout variant={ variant } />;

export default withLayout(mapLayoutStateToLayoutTree, { variant: "light" })(About);
```

## Tests
Expand Down
10 changes: 0 additions & 10 deletions babel.config.js

This file was deleted.

5 changes: 2 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const { compose, baseConfig, withRTL } = require('@moxy/jest-config');
const { compose, baseConfig, withEnzyme } = require('@moxy/jest-config');

module.exports = compose([
baseConfig,
withRTL,
withEnzyme('enzyme-adapter-react-16'),
]);

Loading

0 comments on commit 3ac0e7a

Please sign in to comment.