Skip to content

Commit

Permalink
feat: add chainEventHandlers helper function
Browse files Browse the repository at this point in the history
fix #49
  • Loading branch information
jedwards1211 committed Mar 19, 2024
1 parent d2c5853 commit 269b36d
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 0 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ For MUI v4 you'll need `material-ui-popup-state@^1.9.3`.
- [`disableAutoFocus` (`boolean`, **optional**)](#disableautofocus-boolean-optional-1)
- [`children` (`(popupState: InjectedProps) => ?React.Node`, **required**)](#children-popupstate-injectedprops--reactnode-required)
- [Using `Popover` and `Menu` with `bindHover`](#using-popover-and-menu-with-bindhover)
- [Chaining event handlers](#chaining-event-handlers)
- [Chaining event handlers manually](#chaining-event-handlers-manually)
- [Using `material-ui-popup-state/chainEventHandlers`](#using-material-ui-popup-statechaineventhandlers)

<!-- tocstop -->

Expand Down Expand Up @@ -590,3 +593,52 @@ import HoverPopover from 'material-ui-popup-state/HoverPopover'
```

These are just wrapper components that pass inline styles to prevent `Modal` from blocking pointer events.

# Chaining event handlers

What if you need to perform additional actions in `onClick`, but it's being injected by `{...bindTrigger(popupState)}` etc?

There are two options:

## Chaining event handlers manually

This is the most straightforward, explicit option.

```tsx
const button = (
<Button
{...bindTrigger(popupState)}
onClick={(e: React.MouseEvent) => {
bindTrigger(popupState).onClick(e)
performCustomAction(e)
}}
>
Open Menu
</Button>
)
```

## Using `material-ui-popup-state/chainEventHandlers`

If you don't like the above option, you can use the provided `material-ui-popup-state/chainEventHandlers` helper:

```tsx
import { chainEventHandlers } from 'material-ui-popup-state/chainEventHandlers'

const button = (
<Button
{...chainEventHandlers(bindTrigger(popupState), {
onClick: (e: React.MouseEvent) => {
bindTrigger(popupState).onClick(e)
performCustomAction(e)
},
})}
>
Open Menu
</Button>
)
```

`chainEventHandlers` accepts a variable number of props arguments and combines any function props of the same name
into a function that invokes the chained functions in sequence. For all other properties the behavior is like
`Object.assign`.
8 changes: 8 additions & 0 deletions demo/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import Dialog from './examples/Dialog'
import DialogCode from '!!raw-loader!./examples/Dialog'
import DialogHooks from './examples/Dialog.hooks'
import DialogHooksCode from '!!raw-loader!./examples/Dialog.hooks'
import ChainingEventHandlers from './examples/ChainingEventHandlers'
import ChainingEventHandlersCode from '!!raw-loader!./examples/ChainingEventHandlers'
import Demo from './Demo'
import Typography from '@mui/material/Typography'
import { withStyles } from '@mui/styles'
Expand Down Expand Up @@ -157,6 +159,12 @@ const Root = ({ classes }) => (
hooksExample={<DialogHooks />}
hooksCode={DialogHooksCode}
/>
<Demo
title="Chaining Event Handlers"
headerId="chaining-event-handlers"
example={<ChainingEventHandlers />}
code={ChainingEventHandlersCode}
/>
</div>
</ThemeProvider>
</StyledEngineProvider>
Expand Down
51 changes: 51 additions & 0 deletions demo/examples/ChainingEventHandlers.hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react'
import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog'
import DialogTitle from '@mui/material/DialogTitle'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogActions from '@mui/material/DialogActions'
import {
usePopupState,
bindTrigger,
bindDialog,
} from 'material-ui-popup-state/hooks'
import { useChainEventHandlers } from 'material-ui-popup-state/useChainEventHandlers'

const DialogPopupState = () => {
const popupState = usePopupState({ variant: 'dialog' })
const boundDialogProps = useChainEventHandlers(
bindDialog(popupState),
{ onClose: () => setTimeout(() => alert('closed dialog!'), 10) },
[popupState]
)
return (
<React.Fragment>
<Button variant="contained" {...bindTrigger(popupState)}>
Open Dialog
</Button>
<Dialog
{...boundDialogProps}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
Chaining Event Handlers example
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
After you close this dialog, you should get an alert
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={popupState.close}>Cancel</Button>
<Button onClick={popupState.close} autoFocus>
OK
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
)
}

export default DialogPopupState
45 changes: 45 additions & 0 deletions demo/examples/ChainingEventHandlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from 'react'
import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog'
import DialogTitle from '@mui/material/DialogTitle'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogActions from '@mui/material/DialogActions'
import PopupState, { bindTrigger, bindDialog } from 'material-ui-popup-state'
import { chainEventHandlers } from 'material-ui-popup-state/chainEventHandlers'

const DialogPopupState = () => (
<PopupState variant="dialog">
{(popupState) => (
<React.Fragment>
<Button variant="contained" {...bindTrigger(popupState)}>
Open Dialog
</Button>
<Dialog
{...chainEventHandlers(bindDialog(popupState), {
onClose: () => setTimeout(() => alert('closed dialog!'), 10),
})}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
Chaining Event Handlers example
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
After you close this dialog, you should get an alert
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={popupState.close}>Cancel</Button>
<Button onClick={popupState.close} autoFocus>
OK
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
)}
</PopupState>
)

export default DialogPopupState
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@
"types": "./dist/HoverPopover.d.ts",
"import": "./dist/HoverPopover.mjs",
"default": "./dist/HoverPopover.js"
},
"./chainEventHandlers": {
"types": "./dist/chainEventHandlers.d.ts",
"import": "./dist/chainEventHandlers.mjs",
"default": "./dist/chainEventHandlers.js"
}
},
"engines": {
Expand Down
88 changes: 88 additions & 0 deletions src/chainEventHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable @typescript-eslint/ban-types */
export function chainEventHandlers<T1 extends {}, T2 extends {}>(
t1: T1,
t2: T2
): T1 & T2
export function chainEventHandlers<T1 extends {}, T2 extends {}, T3 extends {}>(
t1: T1,
t2: T2,
t3: T3
): T1 & T2 & T3
export function chainEventHandlers<
T1 extends {},
T2 extends {},
T3 extends {},
T4 extends {}
>(t1: T1, t2: T2, t3: T3, t4: T4): T1 & T2 & T3 & T4
export function chainEventHandlers<
T1 extends {},
T2 extends {},
T3 extends {},
T4 extends {},
T5 extends {}
>(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): T1 & T2 & T3 & T4 & T5
export function chainEventHandlers<
T1 extends {},
T2 extends {},
T3 extends {},
T4 extends {},
T5 extends {},
T6 extends {}
>(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6): T1 & T2 & T3 & T4 & T5 & T6
export function chainEventHandlers<
T1 extends {},
T2 extends {},
T3 extends {},
T4 extends {},
T5 extends {},
T6 extends {},
T7 extends {}
>(
t1: T1,
t2: T2,
t3: T3,
t4: T4,
t5: T5,
t6: T6,
t7: T7
): T1 & T2 & T3 & T4 & T5 & T6 & T7
export function chainEventHandlers<
T1 extends {},
T2 extends {},
T3 extends {},
T4 extends {},
T5 extends {},
T6 extends {},
T7 extends {},
T8 extends {}
>(
t1: T1,
t2: T2,
t3: T3,
t4: T4,
t5: T5,
t6: T6,
t7: T7,
t8: T8
): T1 & T2 & T3 & T4 & T5 & T6 & T7 & T8
export function chainEventHandlers(
first: Record<string, any>,
...rest: Record<string, any>[]
): Record<string, any> {
const result: Record<string, any> = { ...first }
for (const obj of rest) {
for (const key in obj) {
const value = obj[key]
const prev = result[key]
if (typeof prev === 'function' && typeof value === 'function') {
result[key] = (...args: any[]) => {
prev(...args)
return value(...args)
}
} else {
result[key] = value
}
}
}
return result
}
55 changes: 55 additions & 0 deletions test/chainEventHandlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it } from 'mocha'
import { expect } from 'chai'
import { chainEventHandlers } from '../src/chainEventHandlers'

describe(`chainEventHandlers`, function () {
it(`works`, function () {
const calls: [string, any][] = []
const combined = chainEventHandlers(
{
a: 1,
b: 3,
onClick: (arg: any) => {
calls.push(['a', arg])
},
},
{
a: 2,
onClick: (arg: any) => {
calls.push(['b', arg])
},
onBlah: (arg: any) => {
calls.push(['blah-b', arg])
},
},
{
c: 4,
onClick: (arg: any) => {
calls.push(['c', arg])
},
onBlah: (arg: any) => {
calls.push(['blah-c', arg])
},
}
)
expect(Object.keys(combined).sort()).to.deep.equal(
['a', 'b', 'c', 'onClick', 'onBlah'].sort()
)
expect(combined.a).to.equal(2)
expect(combined.b).to.equal(3)
expect(combined.c).to.equal(4)

combined.onClick('foo')
expect(calls).to.deep.equal([
['a', 'foo'],
['b', 'foo'],
['c', 'foo'],
])
calls.length = 0
combined.onBlah('blah')
expect(calls).to.deep.equal([
['blah-b', 'blah'],
['blah-c', 'blah'],
])
})
})

0 comments on commit 269b36d

Please sign in to comment.