1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Added IoC example (#3595)

* Added `with-ioc` example

* pre-compile deps until we get nextjs magic working
This commit is contained in:
Alex Indigo 2018-01-30 23:36:20 -08:00 committed by Tim Neutkens
parent 4b143fc232
commit e401e2cf5f
20 changed files with 508 additions and 0 deletions

View file

@ -0,0 +1,16 @@
{
"env": {
"development": {
"presets": ["next/babel"],
"plugins": ["transform-decorators-legacy"]
},
"production": {
"presets": ["next/babel"],
"plugins": ["transform-decorators-legacy"]
},
"test": {
"presets": [["next/babel", { "preset-env": { "modules": "commonjs" } }]],
"plugins": ["transform-decorators-legacy"]
}
}
}

View file

@ -0,0 +1,43 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-ioc)
# Dependency Injection (IoC) example ([ioc](https://github.com/alexindigo/ioc))
## How to use
### Using `create-next-app`
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
```
npm i -g create-next-app
create-next-app --example with-ioc with-ioc-app
```
### Download manually
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-ioc
cd with-ioc
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
This example uses [ioc](https://github.com/alexindigo/ioc) for dependency injection, which lets you create decoupled shared components and keep them free from implementation details of your app / other components.
It builds on top of [with-next-routes](https://github.com/zeit/next.js/tree/master/examples/with-next-routes) example and makes use of dependency injection to propagate custom `Link` component to other components.
Also, it illustrates ergonomics of testing using dependency injection.

View file

@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`With Snapshot Testing Blog renders components 1`] = `
<div>
<h1>
Hi There!
</h1>
<div
style={
Object {
"border": "1px dotted #ff0000",
"marginTop": "5px",
"padding": "10px",
}
}
>
<h3>
Component1
</h3>
Knows nothing about any custom \`Link\` or \`Router\` components or prop
<div
style={
Object {
"border": "1px dashed #0000ff",
"marginTop": "5px",
"padding": "10px",
}
}
>
<h3>
Component2
</h3>
Knows nothing about any custom \`Link\` or \`Router\` components or prop
<div
style={
Object {
"border": "1px dashed #00ff00",
"marginTop": "5px",
"padding": "10px",
}
}
>
<h3>
Endpoint
</h3>
Uses injected \`Link\` component without direct dependency on one
<br />
<a
href="/about-us/baz"
onClick={[Function]}
>
About: foo baz
</a>
<br />
<a
href="/"
onClick={[Function]}
>
go Home
</a>
</div>
<div
style={
Object {
"border": "1px dashed #00ff00",
"marginTop": "5px",
"padding": "10px",
}
}
>
<h3>
EndButton
</h3>
Uses injected \`Router\` component without direct dependency on one
<br />
<button
onClick={[Function]}
>
Route to About foo bar
</button>
<br />
<button
onClick={[Function]}
>
go Home
</button>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`With Snapshot Testing Blog renders components 1`] = `
<div
style={
Object {
"border": "1px dashed #00ff00",
"marginTop": "5px",
"padding": "10px",
}
}
>
<h3>
Endpoint
</h3>
Uses injected \`Link\` component without direct dependency on one
<br />
<div
comment="mocked Link component"
>
<a>
About: foo baz
</a>
</div>
<br />
<div
comment="mocked Link component"
>
<a>
go Home
</a>
</div>
</div>
`;

View file

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`With Snapshot Testing App shows "Menu" 1`] = `
<ul>
<li>
<a
href="/blog/hello-world"
onClick={[Function]}
>
Blog: Hello world
</a>
</li>
<li>
<a
href="/blog/another-blog-post"
onClick={[Function]}
>
Blog: Another blog post
</a>
</li>
<li>
<a
href="/blog/non-existant"
onClick={[Function]}
>
Blog: Not found
</a>
</li>
<li>
<button
onClick={[Function]}
>
About foo bar
</button>
</li>
<li>
<button
onClick={[Function]}
>
About foo baz
</button>
</li>
</ul>
`;

View file

@ -0,0 +1,35 @@
/* eslint-env jest */
/*
* Testing pages with @provide decorator:
*
* Snapshots as usual
*
* Shallow rendering need to `.dive()` one level deep,
* as with any High Order Component.
* Also `.html()` may cause havoc when it'd try to expand the render
* but won't inject context since top level co,ponent has been rendered already.
* This problem is not unique to IoC though, anything that relies on context (i.e. Redux)
* is facing the same issue. Use `.debug()` or `mount()` instead
*/
import { shallow } from 'enzyme'
import React from 'react'
import renderer from 'react-test-renderer'
import App from '../pages/blog.js'
describe('With Enzyme', () => {
it('Blog renders components', () => {
const app = shallow(<App post={{title: 'Hi There!'}} />).dive()
expect(app.find('h1').text()).toEqual('Hi There!')
})
})
describe('With Snapshot Testing', () => {
it('Blog renders components', () => {
const component = renderer.create(<App post={{title: 'Hi There!'}} />)
const tree = component.toJSON()
expect(tree).toMatchSnapshot()
})
})

View file

@ -0,0 +1,36 @@
/* eslint-env jest */
/*
* Individual component testing is pretty simple
* just provide your dependencies as props
* and add `.dive()` step to your shallow render,
* as with any High Order Component.
*
* Remarks about `.html()` may apply,
* depending if any of the children components
* expect anything from the context
*/
import { shallow } from 'enzyme'
import React from 'react'
import renderer from 'react-test-renderer'
import Component from '../components/endpoint.js'
describe('With Enzyme', () => {
it('Component renders with props', () => {
// no need to mock Link component much for shallow rendering
const injected = shallow(<Component Link={() => {}} />)
const component = injected.dive()
expect(component.find('h3').text()).toEqual('Endpoint')
expect(component.find('Link').first().find('a').text()).toEqual('About: foo baz')
})
})
describe('With Snapshot Testing', () => {
it('Blog renders components', () => {
const component = renderer.create(<Component Link={(props) => <div comment={'mocked Link component'}>{props.children}</div>} />)
const tree = component.toJSON()
expect(tree).toMatchSnapshot()
})
})

View file

@ -0,0 +1,26 @@
/* eslint-env jest */
/*
* Testing pages without @provide decorator as usual
*/
import { shallow } from 'enzyme'
import React from 'react'
import renderer from 'react-test-renderer'
import App from '../pages/index.js'
describe('With Enzyme', () => {
it('App shows "Menu"', () => {
const app = shallow(<App />)
expect(app.find('li a').first().text()).toEqual('Blog: Hello world')
})
})
describe('With Snapshot Testing', () => {
it('App shows "Menu"', () => {
const component = renderer.create(<App />)
const tree = component.toJSON()
expect(tree).toMatchSnapshot()
})
})

View file

@ -0,0 +1,10 @@
import React from 'react'
import Component2 from './component2'
export default () => (
<div style={{ marginTop: '5px', border: '1px dotted #ff0000', padding: '10px' }}>
<h3>Component1</h3>
Knows nothing about any custom `Link` or `Router` components or prop
<Component2 />
</div>
)

View file

@ -0,0 +1,12 @@
import React from 'react'
import Endpoint from './endpoint'
import EndButton from './endbutton'
export default () => (
<div style={{ marginTop: '5px', border: '1px dashed #0000ff', padding: '10px' }}>
<h3>Component2</h3>
Knows nothing about any custom `Link` or `Router` components or prop
<Endpoint />
<EndButton />
</div>
)

View file

@ -0,0 +1,23 @@
import React from 'react'
import { inject } from 'ioc'
import PropTypes from 'prop-types'
@inject({
Router: PropTypes.object
})
export default class extends React.Component {
render () {
const { Router } = this.props
return (
<div style={{ marginTop: '5px', border: '1px dashed #00ff00', padding: '10px' }}>
<h3>EndButton</h3>
Uses injected `Router` component without direct dependency on one
<br />
<button onClick={() => Router.pushRoute('about', { foo: 'bar' })}>Route to About foo bar</button>
<br />
<button onClick={() => Router.pushRoute('/')}>go Home</button>
</div>
)
}
}

View file

@ -0,0 +1,29 @@
import React from 'react'
import { inject } from 'ioc'
import PropTypes from 'prop-types'
@inject({
// keep it `isRequired`-free to allow mock injection via props
Link: PropTypes.func
})
export default class extends React.Component {
static propTypes = {
// you can add `isRequired` to the component's propTypes definition
Link: PropTypes.func.isRequired
}
render () {
const { Link } = this.props
return (
<div style={{ marginTop: '5px', border: '1px dashed #00ff00', padding: '10px' }}>
<h3>Endpoint</h3>
Uses injected `Link` component without direct dependency on one
<br />
<Link route='about' params={{ foo: 'baz' }}><a>About: foo baz</a></Link>
<br />
<Link route='/'><a>go Home</a></Link>
</div>
)
}
}

View file

@ -0,0 +1,4 @@
module.exports = {
setupFiles: ['<rootDir>/jest.setup.js'],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/']
}

View file

@ -0,0 +1,4 @@
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
configure({ adapter: new Adapter() })

View file

@ -0,0 +1,26 @@
{
"name": "with-ioc",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"test": "NODE_ENV=test jest",
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"ioc": "1.0.3",
"next": "latest",
"next-routes": "^1.0.17",
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
"devDependencies": {
"babel-plugin-transform-decorators-legacy": "1.3.4",
"enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1",
"jest": "22.1.3",
"react-addons-test-utils": "15.6.2",
"react-test-renderer": "16.2.0"
}
}

View file

@ -0,0 +1 @@
export default props => <h1>About foo {props.url.query.foo} no `Link` ({typeof props.Link}) available here</h1>

View file

@ -0,0 +1,41 @@
import React from 'react'
import { provide, types } from 'ioc'
import { Link, Router } from '../routes'
import Component1 from '../components/component1'
const posts = [
{ slug: 'hello-world', title: 'Hello world' },
{ slug: 'another-blog-post', title: 'Another blog post' }
]
@provide({
@types.func.isRequired
Link,
@types.object
Router
})
export default class extends React.Component {
static async getInitialProps ({ query, res }) {
const post = posts.find(post => post.slug === query.slug)
if (!post && res) {
res.statusCode = 404
}
return { post }
}
render () {
const { post } = this.props
if (!post) return <h1>Post not found</h1>
return (
<div>
<h1>{post.title}</h1>
<Component1 />
</div>
)
}
}

View file

@ -0,0 +1,11 @@
import { Link, Router } from '../routes'
export default () => (
<ul>
<li><Link route='blog' params={{ slug: 'hello-world' }}><a>Blog: Hello world</a></Link></li>
<li><Link route='blog' params={{ slug: 'another-blog-post' }}><a>Blog: Another blog post</a></Link></li>
<li><Link route='blog' params={{ slug: 'non-existant' }}><a>Blog: Not found</a></Link></li>
<li><button onClick={() => Router.pushRoute('about', { foo: 'bar' })}>About foo bar</button></li>
<li><button onClick={() => Router.pushRoute('about', { foo: 'baz' })}>About foo baz</button></li>
</ul>
)

View file

@ -0,0 +1,5 @@
const nextRoutes = require('next-routes')
const routes = module.exports = nextRoutes()
routes.add('blog', '/blog/:slug')
routes.add('about', '/about-us/:foo(bar|baz)')

View file

@ -0,0 +1,17 @@
const { createServer } = require('http')
const next = require('next')
const routes = require('./routes')
const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handler = routes.getRequestHandler(app)
app.prepare()
.then(() => {
createServer(handler)
.listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})