Skip to content

Commit

Permalink
feat: add a modal provider (#31)
Browse files Browse the repository at this point in the history
* feat: add a modal provider

* feat: pass down entire modal. throw on invalid name

* feat: closeAllModals. better types. only pass down modal

* feat: update README

* chore: be consistent (sorry 😅)

* fix: typo

* feat: use <>
  • Loading branch information
nicosantangelo authored and cazala committed Jan 30, 2019
1 parent c026246 commit f50cc0e
Show file tree
Hide file tree
Showing 17 changed files with 283 additions and 14 deletions.
92 changes: 91 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Common modules for our dApps
- [Analytics](https://github.com/decentraland/decentraland-dapps#analytics)
- [Loading](https://github.com/decentraland/decentraland-dapps#loading)
- [Location](https://github.com/decentraland/decentraland-dapps#location)
- [Modal](https://github.com/decentraland/decentraland-dapps#modal)
- [Lib](https://github.com/decentraland/decentraland-dapps#lib)
- [API](https://github.com/decentraland/decentraland-dapps#api)
- [Containers](https://github.com/decentraland/decentraland-dapps#lib)
Expand Down Expand Up @@ -944,7 +945,7 @@ isRoot(state)

### Installation

You need to add a reducer and a saga to use this module
In order to use this module you need to add a reducer and a saga.

**Reducer**:

Expand Down Expand Up @@ -1001,6 +1002,95 @@ This way you can change the default locations to use different ones. This will b
</p>
</details>

## Modal

Leverages redux state and provides actions to open and close each modal by name. It provides two simple actions:

```ts
openModal(name: string, metadata: any = null)
closeModal(name: string)
closeAllModals()
```

It also provides a selector to get the open modals:

```
getOpenModals(state): ModalState
```

### Installation

In order to use this module you need to add a reducer and a provider.

**Provider**:

Add the `<ModalProvider>` as a parent of your routes. It takes an object of `{ {modalName: string]: React.Component }` as a prop (`components`). It'll use it to render the appropiate modal when you call `openModal(name: string)`

```tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'react-router-redux'
import ModalProvider from 'decentraland-dapps/dist/providers/ModalProvider'
import * as modals from 'components/Modals'
import { store, history } from './store'

ReactDOM.render(
<Provider store={store}>
<ModalProvider components={modals}>
<ConnectedRouter history={history}>{/* Your App */}</ConnectedRouter>
</ModalProvider>
</Provider>,
document.getElementById('root')
)
```

where `modals` could look like this:

```ts
// components/Modals/index.ts

export { default as HelpModal } from './HelpModal'
```

Each modal will receive the properties defined on the `ModalComponent` type, found on `modules/modal/types`, so for example:

```tsx
import { ModalProps } from 'decentraland-dapps/dist/modules/modal/types'

type HelpModalProps = ModalProps & {
// Some custom props, maybe from a container
}

export default class HelpModal extends React.Component<HelpModalProps> {
onClose = () => {
const { modal, onClose } = this.props
closeModal(modal.name)
}

render() {
const { modal } = this.props
// Do something with modal.metadata
// The Modal component here can be whatever you like, just make sure to call closeModal(name) when you want to close it, to update the state
return <Modal open={modal.open} onClose={onClose} />
}
}
```

**Reducer**:

Add the `modalReducer` as `modal` to your `rootReducer`:

```ts
import { combineReducers } from 'redux'
import { modalReducer as modal } from 'decentraland-dapps/dist/modules/modal/reducer'

export const rootReducer = combineReducers({
modal
// your other reducers
})
```

# Lib

Common libraries for dApps
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"version": "0.0.0-development",
"main": "dist",
"dependencies": {
"@types/axios": "^0.14.0",
"@types/flat": "0.0.28",
"@types/node": "^10.1.2",
"@types/react": "^16.4.7",
Expand Down Expand Up @@ -39,7 +38,7 @@
"chai": "^4.1.2",
"dcl-tslint-config-standard": "^1.0.1",
"decentraland-eth": "^6.0.0",
"decentraland-ui": "^1.15.0",
"decentraland-ui": "^1.16.2",
"husky": "^0.14.3",
"mocha": "^5.2.0",
"nyc": "^12.0.2",
Expand All @@ -58,7 +57,7 @@
},
"peerDependencies": {
"decentraland-eth": ">=6.0.0",
"decentraland-ui": "^1.15.0",
"decentraland-ui": "^1.16.2",
"react": "^16.4.1",
"react-redux": "^5.0.7",
"react-router-redux": "^5.0.0-alpha.6",
Expand Down
1 change: 0 additions & 1 deletion src/containers/EtherscanLink/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import EtherscanLink from './EtherscanLink.container'

export default EtherscanLink
29 changes: 29 additions & 0 deletions src/modules/modal/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { action } from 'typesafe-actions'

// Open Modal

export const OPEN_MODAL = 'Open Modal'

export const openModal = (name: string, metadata: any = null) =>
action(OPEN_MODAL, {
name,
metadata
})

export type OpenModalAction = ReturnType<typeof openModal>

// Close Modal

export const CLOSE_MODAL = 'Close Modal'

export const closeModal = (name: string) => action(CLOSE_MODAL, { name })

export type CloseModalAction = ReturnType<typeof closeModal>

// Close All Modals

export const CLOSE_ALL_MODALS = 'Close All Modal'

export const closeAllModals = () => action(CLOSE_ALL_MODALS, {})

export type CloseAllModalsAction = ReturnType<typeof closeAllModals>
63 changes: 63 additions & 0 deletions src/modules/modal/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
OPEN_MODAL,
CLOSE_MODAL,
CLOSE_ALL_MODALS,
OpenModalAction,
CloseModalAction,
CloseAllModalsAction
} from './actions'
import { Modal } from './types'

export type ModalState = Record<string, Modal>

const INITIAL_STATE: ModalState = {}

export type ModalReducerAction =
| OpenModalAction
| CloseModalAction
| CloseAllModalsAction

export function modalReducer(
state = INITIAL_STATE,
action: ModalReducerAction
) {
switch (action.type) {
case OPEN_MODAL: {
const { name, metadata } = action.payload

return {
...state,
[name]: {
open: true,
name,
metadata
}
}
}
case CLOSE_MODAL: {
const { name } = action.payload

if (state[name]) {
return {
...state,
[name]: {
...state[name],
open: false
}
}
} else {
// Invalid modal name
return state
}
}
case CLOSE_ALL_MODALS: {
const newState = {}
for (const name in state) {
newState[name] = { ...state[name], open: false }
}
return newState
}
default:
return state
}
}
16 changes: 16 additions & 0 deletions src/modules/modal/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ModalState } from './reducer'

export const getState: (state: any) => ModalState = state => state.modal
export const getOpenModals: (state: any) => ModalState = state => {
const openModals = {}
const modals = getState(state)

for (const name in modals) {
const modal = modals[name]
if (modal.open) {
openModals[name] = modal
}
}

return openModals
}
9 changes: 9 additions & 0 deletions src/modules/modal/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type Modal = {
open: boolean
name: string
metadata: any
}

export type ModalProps = {
modal: Modal
}
16 changes: 16 additions & 0 deletions src/providers/ModalProvider/ModalProvider.container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { connect } from 'react-redux'
import { RootDispatch } from '../../types'
import { getState as getModals } from '../../modules/modal/selectors'
import { MapStateProps, MapDispatchProps } from './ModalProvider.types'
import ModalProvider from './ModalProvider'

const mapState = (state: any): MapStateProps => ({
modals: getModals(state)
})

const mapDispatch = (_: RootDispatch): MapDispatchProps => ({})

export default connect(
mapState,
mapDispatch
)(ModalProvider) as any
36 changes: 36 additions & 0 deletions src/providers/ModalProvider/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react'

import { ModalProps } from '../../modules/modal/types'
import { DefaultProps, Props } from './ModalProvider.types'

export default class ModalProvider extends React.PureComponent<Props> {
static defaultProps: DefaultProps = {
children: null
}

render() {
const { children, components, modals } = this.props

const ModalComponents: JSX.Element[] = []

for (const name in modals) {
const modal = modals[name]
let ModalComponent: React.ComponentType<ModalProps> = components[name]

if (!ModalComponent) {
if (name) {
throw new Error(`Couldn't find a modal Component named "${name}"`)
}
}

ModalComponents.push(<ModalComponent key={name} modal={modal} />)
}

return (
<>
{children}
{ModalComponents}
</>
)
}
}
14 changes: 14 additions & 0 deletions src/providers/ModalProvider/ModalProvider.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ModalState } from '../../modules/modal/reducer'
import { ModalProps } from '../../modules/modal/types'

export type DefaultProps = {
children: React.ReactNode | null
}

export type Props = DefaultProps & {
components: Record<string, React.ComponentType<ModalProps>>
modals: ModalState
}

export type MapStateProps = Pick<Props, 'modals'>
export type MapDispatchProps = {}
2 changes: 2 additions & 0 deletions src/providers/ModalProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import ModalProvider from './ModalProvider.container'
export default ModalProvider
4 changes: 2 additions & 2 deletions src/providers/TranslationProvider/TranslationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export default class TranslationProvider extends React.PureComponent<Props> {

return translations ? (
<I18nProvider locale={locale} messages={translations}>
<React.Fragment>
<>
<TranslationSetup />
{children}
</React.Fragment>
</>
</I18nProvider>
) : (
this.renderLoading()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Locale } from 'decentraland-ui'
import { fetchTranslationsRequest } from '../../modules/translation/actions'
import { TranslationKeys } from '../../modules/translation/types'

export interface Props {
export type Props = {
locale?: Locale
locales: Locale[]
translations?: TranslationKeys
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import TranslationSetup from './TranslationSetup.container'

export default TranslationSetup
1 change: 0 additions & 1 deletion src/providers/TranslationProvider/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import TranslationProvider from './TranslationProvider.container'

export default TranslationProvider
5 changes: 2 additions & 3 deletions src/providers/WalletProvider/WalletProvider.types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { connectWalletRequest } from '../../modules/wallet/actions'

export interface DefaultProps {
export type DefaultProps = {
children: React.ReactNode | null
}

export interface Props extends DefaultProps {
export type Props = DefaultProps & {
onConnect: typeof connectWalletRequest
}

export type MapStateProps = {}
export type MapDispatchProps = Pick<Props, 'onConnect'>

1 change: 0 additions & 1 deletion src/providers/WalletProvider/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import WalletProvider from './WalletProvider.container'

export default WalletProvider

0 comments on commit f50cc0e

Please sign in to comment.