Skip to content

Instantly share code, notes, and snippets.

@codfish
Last active April 30, 2021 21:17
Show Gist options
  • Save codfish/2fc8b7598b79948f33824254fadaea8e to your computer and use it in GitHub Desktop.
Save codfish/2fc8b7598b79948f33824254fadaea8e to your computer and use it in GitHub Desktop.
React Module component with context

Why this approach?

If our Box is an atom, the Module component is a micro-extension of a Box which you might consider a molecule. It's implementing some of the common patterns we've noticed across all these modules that should help us from repeating ourselves and allow us to build out screens faster. Very similar to Card which might be a more familiar concept, but even simpler with less variations for things like media.

Why this approach?

Composability. Breaking up what might seem like a single concept of a Module into multiple components like this helps us to maintain AND test them more easily.

It also provides the consumer developers & applications the flexibility they need to build out screens the way they want to without having a single massive component made up of many components, which we need to control through schemas, or passing nested props, or having tons of slots. All of which creates complexities in testing and maintaining a component without breaking things.

Module, ModuleHeader, ModuleContent, ModuleFooter can be separate components. Not every Module implemented needs to use all of them. Some modules don't have headers or footers, etc. This is a very powerful composability pattern used by popular UI component libraries to improve testability and maintainability while giving end users the a lot of flexibility in the cleanest possible way. The resulting usage becomes much clearer, as opposed to a single component with many props, slots and/or schemas, you've got a very declarative composition of components.

  • Optional expand/collapse.
  • The resulting usage becomes much clearer, as opposed to a single component with many props, slots and/or schemas, you've got a very declarative composition of components.
<Module expandable closed>
  <ModuleHeader>Some Header Here</ModuleHeader>

  <ModuleContent>
    <p>Anything you want here</p>
  </ModuleContent>
  
  <ModuleFooter>Something here?</ModuleFooter>
</Module>
  • Each component will have it's own props and focus. Make the most common patterns be the defaults so users don't need to specify anything in those scenarios.

Basic, not expandable

<Module>
  <ModuleHeader>Some Header Here</ModuleHeader>

  <ModuleContent>
    <p>Anything you want here</p>
  </ModuleContent>
</Module>

Expandable, but closed by default

<Module expandable closed>
  <ModuleHeader>Some Header Here</ModuleHeader>

  <ModuleContent>
    <p>Anything you want here</p>
  </ModuleContent>
</Module>

Props for header

<Module>
  <ModuleHeader subtitle="Some subtitle here" actions={(
    <Link href={`/contracts/${id}`}>
      Edit Contract Terms
    </Link>
  )}>
    Some Header Here
  </ModuleHeader>

  <ModuleContent>
    <p>Anything you want here</p>
  </ModuleContent>
</Module>
import React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import styles from './Module.style';
import { Provider } from '../context/module';
const Module = ({ expandable, closed, children, className: classNameProp, ...other }) => {
const className = clsx(styles.root, classNameProp);
return (
<Provider expandable={expandable} closed={closed}>
<div className={className} {...other}>
{children}
</div>
</Provider>
);
};
Module.propTypes = {
expandable: PropTypes.bool,
closed: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.node.isRequired,
};
Module.defaultProps = {
expandable: false,
closed: false,
className: '',
};
export default Module;
import { mergeStyleSets } from '@uifabric/merge-styles';
import theme from '../../theme.style';
export default mergeStyleSets({
root: {
backgroundColor: theme.palette.neutralWhite,
},
});
import React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import styles from './ModuleContent.style';
import { useModuleContext } from '../context/module';
const ModuleContent = ({ children, className: classNameProp, ...other }) => {
const { isOpen } = useModuleContext();
const className = clsx(styles.root, classNameProp, {
[styles.isClosed]: !isOpen,
[styles.isOpen]: isOpen,
});
return (
<div className={className} {...other}>
{children}
</div>
);
};
ModuleContent.propTypes = {
className: PropTypes.string,
children: PropTypes.node.isRequired,
};
ModuleContent.defaultProps = {
className: '',
};
export default ModuleContent;
import { mergeStyleSets } from '@uifabric/merge-styles';
import { pxToRem } from '../../utils';
import theme, { layout } from '../../theme.style';
export default mergeStyleSets({
root: {
padding: pxToRem(16),
paddingBottom: pxToRem(24),
selectors: {
[layout.breakpoints.min.md]: {
padding: pxToRem(24),
paddingBottom: pxToRem(32),
},
[layout.breakpoints.min.lg]: {
paddingLeft: pxToRem(40),
paddingRight: pxToRem(40),
},
},
},
isClosed: {
display: 'none',
},
isOpen: {
borderBottom: `${pxToRem(1)} solid ${theme.palette.neutralQuaternary}`,
},
});
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'office-ui-fabric-react/lib/Text';
import { FontIcon } from 'office-ui-fabric-react/lib/Icon';
import clsx from 'clsx';
import styles from './ModuleHeader.style';
import { useModule } from '../context/module';
const ModuleHeader = ({ variant, actions, children, className: classNameProp, ...other }) => {
const { toggleIsOpen, isOpen, disableAccordian } = useModule();
const headerMap = {
medium: 'h4',
large: 'h3',
xLarge: 'h2',
};
const expandable = disableAccordian === false;
const className = clsx(styles.root, classNameProp, {
[styles.isOpen]: isOpen,
});
const handleClick = () => {
if (expandable) {
toggleIsOpen();
}
};
return (
<Text as={headerMap[variant]} onClick={handleClick} className={className} {...other}>
<span>{children}</span>
<div>
{actions}
{expandable && (
<FontIcon
className={styles.icon}
iconName={isOpen ? 'ChevronUpMed' : 'ChevronDownMed'}
tabIndex="0"
/>
)}
</div>
</Text>
);
};
ModuleHeader.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(['medium', 'large', 'xLarge']),
actions: PropTypes.node,
className: PropTypes.string,
};
ModuleHeader.defaultProps = {
variant: 'medium',
actions: null,
className: '',
};
export default ModuleHeader;
import { mergeStyleSets } from '@uifabric/merge-styles';
import { pxToRem } from '../../utils';
import theme, { layout } from '../../theme.style';
export default mergeStyleSets({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginLeft: pxToRem(16),
padding: '1rem 1.5rem 1rem 0',
fontWeight: 'bold',
selectors: {
[layout.breakpoints.min.md]: {
marginLeft: pxToRem(24),
},
},
},
icon: {
height: pxToRem(16),
marginLeft: pxToRem(16),
color: theme.palette.themePrimary,
cursor: 'pointer',
},
isOpen: {
paddingBottom: pxToRem(16),
borderBottom: `${pxToRem(2)} solid ${theme.palette.neutralQuaternary}`,
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment