Skip to content

Instantly share code, notes, and snippets.

@goldhand
Created June 15, 2016 19:29
Show Gist options
  • Save goldhand/1c37ace8b0d3fd39236b1a868c822f63 to your computer and use it in GitHub Desktop.
Save goldhand/1c37ace8b0d3fd39236b1a868c822f63 to your computer and use it in GitHub Desktop.
Flexbox Grid with enhancements implemented in React + Redux.
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;
}
// 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);
};
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>
);
}
}
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;
}
}
// 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