Skip to content

Commit

Permalink
Router: <Set wrap={}>
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe committed Mar 3, 2021
1 parent bcc8dd2 commit 9522b0b
Show file tree
Hide file tree
Showing 10 changed files with 368 additions and 270 deletions.
25 changes: 25 additions & 0 deletions packages/router/src/Set.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { ReactElement, ReactNode, FunctionComponentElement } from 'react'

type WrapperType = (props: { children: any }) => ReactElement | null
type ReduceType = FunctionComponentElement<PropsWithChildren> | undefined

interface Props {
wrap: WrapperType | WrapperType[]
children: ReactNode
}

interface PropsWithChildren {
children: ReactNode
}

export const Set: React.FC<Props> = ({ children, wrap }) => {
const wrappers = Array.isArray(wrap) ? wrap : [wrap]

return (
wrappers.reduceRight<ReduceType>((acc, wrapper) => {
return React.createElement(wrapper, {
children: acc ? acc : children,
})
}, undefined) || null
)
}
45 changes: 43 additions & 2 deletions packages/router/src/__tests__/router.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import '@testing-library/jest-dom/extend-expect'
import { AuthContextInterface } from '@redwoodjs/auth'

import { Router, Route, Private, Redirect, navigate, routes } from '../'
import { resetNamedRoutes } from '../named-routes'
import { Set } from '../Set'
// TODO: This export doesn't exist anymore, do we need it?
// import { resetNamedRoutes } from '../named-routes'

function createDummyAuthContextValues(partial: Partial<AuthContextInterface>) {
const authContextValues: AuthContextInterface = {
Expand Down Expand Up @@ -44,7 +46,7 @@ const mockAuth = (isAuthenticated = false) => {

beforeEach(() => {
window.history.pushState({}, null, '/')
resetNamedRoutes()
// TODO: remove? resetNamedRoutes()
})

test('inits routes and navigates as expected', async () => {
Expand Down Expand Up @@ -87,6 +89,11 @@ test('inits routes and navigates as expected', async () => {
act(() => navigate('/redirect2/redirected'))
await waitFor(() => screen.getByText(/param redirected/i))

// navigate to redirect2 page
// should redirect to /param-test
act(() => navigate('/redirect2/redirected'))
await waitFor(() => screen.getByText(/param redirected/))

act(() => navigate(routes.params({ value: 'one' })))
await waitFor(() => screen.getByText(/param one/i))

Expand Down Expand Up @@ -229,3 +236,37 @@ test('inits routes two private routes with a space in between and loads as expec
// starts on home page
await waitFor(() => screen.getByText(/Home Page/i))
})

test.skip('supports <Set>', async () => {
mockAuth(false)
const GlobalLayout = ({ children }) => (
<div>
<h1>Global Layout</h1>
{children}
</div>
)

const TestRouter = () => (
<Router>
<Set wrap={GlobalLayout}>
<Route path="/" page={HomePage} name="home" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/redirect" page={RedirectPage} name="redirect" />
<Private unauthenticated="home">
<Route path="/private" page={PrivatePage} name="private" />{' '}
<Route path="/another-private" page={PrivatePage} name="private" />
</Private>

<Route
path="/param-test/:value"
page={({ value }) => <div>param {value}</div>}
name="params"
/>
</Set>
</Router>
)
const screen = render(<TestRouter />)

await waitFor(() => screen.getByText(/Global Layout/i))
await waitFor(() => screen.getByText(/Home Page/i))
})
83 changes: 83 additions & 0 deletions packages/router/src/__tests__/set.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { render } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'

import { Set } from '../Set'

// SETUP
const ChildA = () => <h1>ChildA</h1>
const ChildB = () => <h1>ChildB</h1>
const ChildC = () => <h1>ChildC</h1>
const Parent = ({ children }) => <div>{children}</div>
const GlobalLayout: React.FC = ({ children }) => (
<div>
<h1>Global Layout</h1>
{children}
<footer>This is a footer</footer>
</div>
)
const CustomWrapper: React.FC = ({ children }) => (
<div>
<h1>Custom Wrapper</h1>
{children}
<p>Custom Wrapper End</p>
</div>
)
const BLayout = ({ children }) => (
<div>
<h1>Layout for B</h1>
{children}
</div>
)

test('wraps components in other components', async () => {
const TestSet = () => (
<Parent>
<Set wrap={[CustomWrapper, GlobalLayout]}>
<ChildA />
<Set wrap={BLayout}>
<ChildB />
</Set>
</Set>
<ChildC />
</Parent>
)

const screen = render(<TestSet />)

expect(screen.container).toMatchInlineSnapshot(`
<div>
<div>
<div>
<h1>
Custom Wrapper
</h1>
<div>
<h1>
Global Layout
</h1>
<h1>
ChildA
</h1>
<div>
<h1>
Layout for B
</h1>
<h1>
ChildB
</h1>
</div>
<footer>
This is a footer
</footer>
</div>
<p>
Custom Wrapper End
</p>
</div>
<h1>
ChildC
</h1>
</div>
</div>
`)
})
6 changes: 3 additions & 3 deletions packages/router/src/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ const createHistory = () => {
listen: (listener: Listener) => {
const listenerId = 'RW_HISTORY_LISTENER_ID_' + Date.now()
listeners[listenerId] = listener
window?.addEventListener('popstate', listener)
window.addEventListener('popstate', listener)
return listenerId
},
navigate: (to: string) => {
window?.history.pushState({}, '', to)
window.history.pushState({}, '', to)
for (const listener of Object.values(listeners)) {
listener()
}
},
remove: (listenerId: string) => {
if (listeners[listenerId]) {
const listener = listeners[listenerId]
window?.removeEventListener('popstate', listener)
window.removeEventListener('popstate', listener)
delete listeners[listenerId]
} else {
console.warn(
Expand Down
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ export {
PageLoadingContext,
} from './internal'

export * from './Set'

export { usePageLoadingContext } from './page-loader'
1 change: 0 additions & 1 deletion packages/router/src/internal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export * from './util'
export * from './history'
export * from './location'
export * from './named-routes'
export * from './router'
export * from './params'
export * from './links'
Expand Down
30 changes: 8 additions & 22 deletions packages/router/src/location.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,36 +48,22 @@ class LocationProvider extends React.Component<LocationProviderProps> {
}

render() {
const { children } = this.props
const { context } = this.state

return (
<LocationContext.Provider value={context}>
{typeof children === 'function' ? children(context) : children || null}
<LocationContext.Provider value={this.state.context}>
{this.props.children}
</LocationContext.Provider>
)
}
}

interface LocationProps {
children: (context: LocationContextType) => React.ReactChild
}

const Location = ({ children }: LocationProps) => (
<LocationContext.Consumer>
{(context) =>
context ? (
children(context)
) : (
<LocationProvider>{children}</LocationProvider>
)
}
</LocationContext.Consumer>
)

const useLocation = () => {
const location = React.useContext(LocationContext)

if (location === undefined) {
throw new Error('useLocation must be used within a LocationProvider')
}

return location
}

export { Location, LocationProvider, LocationContext, useLocation }
export { LocationProvider, LocationContext, useLocation }
44 changes: 0 additions & 44 deletions packages/router/src/named-routes.tsx

This file was deleted.

72 changes: 72 additions & 0 deletions packages/router/src/router-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useReducer, createContext, useContext } from 'react'

import type { AuthContextInterface, useAuth } from '@redwoodjs/auth'

import { ParamType } from './internal'

const DEFAULT_PAGE_LOADING_DELAY = 1000 // milliseconds

export interface RouterState {
paramTypes?: Record<string, ParamType>
pageLoadingDelay?: number
useAuth: typeof useAuth
}

const RouterStateContext = createContext<RouterState | undefined>(undefined)

export interface RouterSetContextProps {
setState: (newState: Partial<RouterState>) => void
}

const RouterSetContext = createContext<
React.Dispatch<Partial<RouterState>> | undefined
>(undefined)

function stateReducer(state: RouterState, newState: Partial<RouterState>) {
return { ...state, ...newState }
}

export const RouterContextProvider: React.FC<RouterState> = ({
useAuth = () => ({} as AuthContextInterface),
paramTypes,
pageLoadingDelay = DEFAULT_PAGE_LOADING_DELAY,
children,
}) => {
const [state, setState] = useReducer(stateReducer, {
useAuth,
paramTypes,
pageLoadingDelay,
})

return (
<RouterStateContext.Provider value={state}>
<RouterSetContext.Provider value={setState}>
{children}
</RouterSetContext.Provider>
</RouterStateContext.Provider>
)
}

export const useRouterState = () => {
const context = useContext(RouterStateContext)

if (context === undefined) {
throw new Error(
'useRouterState must be used within a RouterContextProvider'
)
}

return context
}

export const useRouterStateSetter = () => {
const context = useContext(RouterSetContext)

if (context === undefined) {
throw new Error(
'useRouterStateSetter must be used within a RouterContextProvider'
)
}

return context
}
Loading

0 comments on commit 9522b0b

Please sign in to comment.