Created
June 15, 2016 19:29
-
-
Save goldhand/1c37ace8b0d3fd39236b1a868c822f63 to your computer and use it in GitHub Desktop.
Flexbox Grid with enhancements implemented in React + Redux.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {Component, PropTypes} from 'react'; | |
import {connect} from 'react-redux'; | |
import {bindActionCreators} from 'redux'; | |
import * as resizeActions from 'actions/windowMonitorActions'; | |
import * as styleOptions from 'constants/styleOptions'; | |
/** | |
* High order component for connecting components to resize events | |
* @constructor | |
* @param {class} ComponentClass - component to decorate | |
*/ | |
export default function connectResizeBreak(ComponentClass) { | |
const refs = { | |
creation: 0, // creation counter | |
}; | |
@connect( | |
state => ({breakScreen: state.windowMonitor.breakScreen}), | |
dispatch => ({actions: bindActionCreators(resizeActions, dispatch)}), | |
) | |
class ResizeBreak extends Component { | |
static propTypes = { | |
breakScreen: PropTypes.oneOf(styleOptions.BREAK_SCREENS_OPTIONS).isRequired, | |
actions: PropTypes.object.isRequired, | |
} | |
componentDidMount() { | |
if (!refs.creation) { | |
addEventListener('resize', this.resize); | |
} | |
refs.creation++; | |
} | |
componentWillUnMount() { | |
refs.creation--; | |
if (!refs.creation) { | |
removeEventListener('resize', this.resize); | |
} | |
} | |
resize = () => { | |
const {actions: {changeBreakScreen}} = this.props; | |
changeBreakScreen(); | |
} | |
render = () => <ComponentClass {...this.props} /> | |
} | |
return ResizeBreak; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// remove duplicate dispatches if an action has meta {checkField, storeName} specified | |
export default store => next => action => { | |
if (!action.meta || !action.meta.nodupes) { | |
return next(action); | |
} | |
let {checkField, storeName} = action.meta; | |
if (!checkField || !storeName) { | |
console.error('You specified a nodupes action but forgot the checkField or storeName argument'); | |
return next(action); | |
} | |
if (action[checkField] === store.getState()[storeName][checkField]) { | |
return {}; | |
} | |
return next(action); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {Component, PropTypes} from 'react'; | |
import * as styleOptions from 'constants/styleOptions'; | |
import connectResizeBreak from 'components/ConnectResize'; | |
const gridStyles = { | |
display: 'flex', | |
flexFlow: 'row wrap', | |
}; | |
/** | |
* Grid for positioning elements in a flex box | |
* | |
* Will determine width needed and pass that as context to children | |
* if a breakPoint is specified, the context width will change to fixedWidth || 100% if the window is below that size | |
* @param children | |
* @param {string} [justifyContent] - flexbox style | |
* @param {string} alignItems - flexbox style | |
* @param {number} [breakPoint] - Point where reponsive styles should change | |
* @param {string} breakScreen - Current active Breakscreen. Used to compare breakPoint against [XS, SM, MD, LG, XL] | |
* @param {number} fixedWidth - Width at breakpoint or override the calculated width if no breakpoint | |
* @param {number} [rows] - Rows of grid. Deactivated by breakPoint | |
* @param {number} [columns] - Columns of grid. Deactived by breakPoint | |
* @param {number} [spacing] - Space between grid panels | |
*/ | |
@connectResizeBreak | |
export default class Grid extends Component { | |
static propTypes = { | |
children: PropTypes.node.isRequired, | |
justifyContent: PropTypes.oneOf(styleOptions.JUSTIFY_CONTENT_OPTIONS).isRequired, | |
alignItems: PropTypes.oneOf(styleOptions.ALIGN_ITEMS_OPTIONS), | |
breakPoint: PropTypes.oneOf(styleOptions.BREAK_POINTS_OPTIONS), | |
breakScreen: PropTypes.oneOf(styleOptions.BREAK_SCREENS_OPTIONS).isRequired, // inherited from @connectResizeBreak | |
fixedWidth: PropTypes.string, | |
rows: PropTypes.number, | |
columns: PropTypes.number, | |
spacing: PropTypes.number, | |
} | |
static defaultProps = { | |
justifyContent: styleOptions.JUSTIFY_CONTENT.BETWEEN, | |
} | |
static childContextTypes = { | |
width: PropTypes.string, | |
spacing: PropTypes.number, | |
isBroke: PropTypes.bool, | |
} | |
getChildContext() { | |
return { | |
width: this.panelWidth(), | |
spacing: this.spacing(), | |
isBroke: this.isBroke(), | |
}; | |
} | |
panelWidth() { | |
const {children, rows, columns, breakPoint, fixedWidth} = this.props; | |
if (breakPoint) { | |
// check if window is smaller than breakpoint | |
if (this.isBroke()) { | |
// return the fixedWidth if specified or 100% width | |
return fixedWidth || '100%'; | |
} | |
} else if (fixedWidth) { | |
// assume all media queries want fixedWidth size | |
return fixedWidth; | |
} | |
let dividend; | |
if (columns) { | |
// if columns were specified use columns to find percentage width of panels | |
dividend = columns; | |
} else if (rows) { | |
// else if rows were specified then find percentage with children count rounded up | |
dividend = Math.ceil(children.length / rows); | |
} else { | |
// nothing was specified, use children.length for one row of even columns | |
dividend = children.length; | |
} | |
return `${100 / dividend}%`; | |
} | |
spacing() { | |
// spacing between panels | |
const {spacing} = this.props; | |
if (this.isBroke()) { | |
return 0; | |
} | |
return spacing; | |
} | |
isBroke() { | |
// return if the breakPoint has been reached | |
const {breakPoint, breakScreen} = this.props; | |
return (breakPoint && styleOptions.BREAK_POINTS[breakScreen] < breakPoint); | |
} | |
render() { | |
const {children, justifyContent, alignItems} = this.props; | |
const styles = { | |
...gridStyles, | |
alignItems, | |
justifyContent, | |
// adjust margin left so grid is flush | |
marginLeft: `-${this.spacing()}px`, | |
}; | |
return ( | |
<div style={styles}> | |
{children} | |
</div> | |
); | |
} | |
} | |
const bgImgStyles = { | |
backgroundPosition: 'center', | |
backgroundSize: 'cover', | |
backgroundRepeat: 'no-repeat', | |
}; | |
const imgStyles = { | |
display: 'block', | |
width: '100%', | |
}; | |
/** | |
* Panel to be used with as child of Grid. | |
* Will set its width to the calulated width provided by Grid. | |
* @param {node} [children] - children components | |
* @param {number} [order] - order component should appear in if isBroke is true | |
* @param {string} [src] - bg image | |
*/ | |
export class GridPanel extends Component { | |
static propTypes = { | |
children: PropTypes.node, | |
order: PropTypes.number, | |
src: PropTypes.string, | |
} | |
static contextTypes = { | |
width: PropTypes.string, | |
spacing: PropTypes.number, | |
isBroke: PropTypes.bool, | |
} | |
order() { | |
const {isBroke} = this.context, {order} = this.props; | |
if (isBroke) return order; | |
} | |
spacing() { | |
const {spacing} = this.context; | |
if (spacing) return `${spacing}px`; | |
} | |
image() { | |
/** | |
* @returns {array} - [imgElem, bgImgStyles] | |
*/ | |
const {isBroke} = this.context, {src} = this.props; | |
if (!src) { | |
// nothing | |
return [null, {}]; | |
} | |
if (isBroke) { | |
// return the actual image, no bg styles | |
return [ | |
<img src={src} style={imgStyles} />, | |
{}, | |
]; | |
} | |
// return the bg styles but no image | |
return [ | |
null, | |
{ | |
...bgImgStyles, | |
backgroundImage: `url(${src})`, | |
}, | |
]; | |
} | |
render() { | |
const | |
{children} = this.props, | |
{width} = this.context, | |
order = this.order(), | |
spacing = this.spacing(), | |
[image, bgStyles] = this.image(); | |
const styles = { | |
width, | |
order, | |
paddingLeft: spacing, | |
paddingBottom: spacing, // TODO: This shouldn't be the same as spacing. Needs to stay active after break | |
...bgStyles, | |
}; | |
return ( | |
<div style={styles}> | |
{image} | |
{children} | |
</div> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as styleOptions from 'constants/styleOptions'; | |
// types | |
const CHANGE_BREAK_POINT = 'CHANGE_BREAK_POINT'; | |
// actions | |
const breakScreen = (width) => { | |
for (let [b, check] of styleOptions.BREAK_SCREENS_MAP.entries()) { | |
if (check(width)) return b; | |
} | |
}; | |
export function changeBreakScreen() { | |
return { | |
type: types.CHANGE_BREAK_POINT, | |
breakScreen: breakScreen(innerWidth), | |
meta: { | |
raf: true, // request animation middleware | |
nodupes: true, // filter redux events with no change middleware | |
checkField: 'breakScreen', | |
storeName: 'windowMonitor', | |
}, | |
}; | |
} | |
// reducer | |
export default function windowMonitor(state = {breakScreen: breakScreen(innerWidth)}, action) { | |
switch (action.type) { | |
case types.CHANGE_BREAK_POINT: | |
return { | |
...state, | |
breakScreen: action.breakScreen, | |
}; | |
default: | |
return state; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Flexbox justify-content options | |
export const JUSTIFY_CONTENT = { | |
START: 'flex-start', | |
END: 'flex-end', | |
CENTER: 'center', | |
BETWEEN: 'space-between', | |
AROUND: 'space-around', | |
}; | |
export const JUSTIFY_CONTENT_OPTIONS = objValues(JUSTIFY_CONTENT); | |
// Flexbox align-items options | |
export const ALIGN_ITEMS = { | |
START: 'flex-start', | |
END: 'flex-end', | |
CENTER: 'center', | |
BASELINE: 'baseline', | |
STREATCH: 'stretch', | |
}; | |
export const ALIGN_ITEMS_OPTIONS = objValues(ALIGN_ITEMS); | |
// Media break points | |
export const BREAK_POINTS = { | |
XL: 1200, | |
LG: 992, | |
MD: 768, | |
SM: 544, | |
XS: 0, | |
}; | |
export const BREAK_POINTS_OPTIONS = objValues(BREAK_POINTS); | |
export const BREAK_SCREENS = { | |
XL: (w) => w >= BREAK_POINTS.XL, | |
LG: (w) => w >= BREAK_POINTS.LG, | |
MD: (w) => w >= BREAK_POINTS.MD, | |
SM: (w) => w >= BREAK_POINTS.SM, | |
XS: (w) => w < BREAK_POINTS.SM, | |
}; | |
export const BREAK_SCREENS_OPTIONS = objKeys(BREAK_SCREENS); | |
export const BREAK_SCREENS_MAP = new Map(Object.entries(BREAK_SCREENS)); | |
/** | |
* Helpers for breaking objects into arrays | |
*/ | |
// returns a generator of the objects keys | |
export function *generateKeys(obj) { | |
let prop; | |
for (prop in obj) { | |
yield prop; | |
} | |
} | |
// returns a generator of the objects values | |
export function *generateValues(obj) { | |
let prop; | |
for (prop in obj) { | |
yield obj[prop]; | |
} | |
} | |
// returns an array of the objects keys | |
export function objKeys(obj) { | |
return Array.from(generateKeys(obj)); | |
} | |
// returns an array of the objects values | |
export function objValues(obj) { | |
return Array.from(generateValues(obj)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment