Skip to content

Instantly share code, notes, and snippets.

@chrisharman
Last active April 2, 2018 04:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrisharman/a303bb3648ddbaf34b86d3a7f959895f to your computer and use it in GitHub Desktop.
Save chrisharman/a303bb3648ddbaf34b86d3a7f959895f to your computer and use it in GitHub Desktop.
React Bootstrap 4 Responsive Layout With Variable Secondary Components
<div id="root"></div>

React Bootstrap 4 Responsive Layout With Variable Secondary Components

Prototype UI for an application that I am building for school.

A Pen by Chris Harman on CodePen.

License.

const { Component } = React;
const ActionButtons = (props) => (
<div className="btn-group border-right" role="group" aria-label="Action buttons">
<button
type="button"
className="btn btn-primary"
style={{ 'borderRadius': '0' }}>
Action 1
</button>
<button
type="button"
className="btn btn-secondary"
style={{ 'borderRadius': '0' }}>
Action 2
</button>
<button
type="button"
className="btn btn-light"
style={{ 'borderRadius': '0' }}>
Action 3
</button>
</div>
);
const SubActionButtons = (props) => (
<div className="col-12">
<div className="btn-group" role="group" aria-label="Sub-action buttons">
<button type="button" className="btn btn-success btn-sm">
Sub-action 1
</button>
<button type="button" className="btn btn-danger btn-sm">
Sub-action 2
</button>
</div>
</div>
);
const Header = (props) => (
<div
className="row border-bottom sticky-top"
style={{ 'backgroundColor': 'Gainsboro' }}>
<div className="col-sm" style={{ 'padding': '0' }}>
<ActionButtons />
</div>
<div className="col-sm">
<h4 className="text-center font-weight-light font-italic">
Project Title
</h4>
</div>
<div className="col-sm"></div>
</div>
);
const SidebarElement = (props) => {
const headingHeight = `${!!props.subActionBtns ? 70 : 30}px`;
return (
<div className="row d-flex flex-fill">
<div className="col border-left border-bottom position-relative">
<div
className="row w-100 border-bottom position-fixed"
style={{ 'backgroundColor': 'Azure', 'height': headingHeight }}>
<div className="col-12">
<h5 className="font-weight-bold" style={{ 'marginBottom': '0.1rem' }}>
{props.heading || 'Sidebar Heading'}
</h5>
</div>
{!!props.subActionBtns ? <SubActionButtons /> : null}
</div>
<div
className="row w-100 position-absolute"
style={{ 'top': headingHeight, 'height': `calc(100% - ${headingHeight})` }}>
<div className="col d-flex flex-column">
<div className="row d-flex flex-fill" style={{ 'overflow': 'auto' }}>
<div style={{ 'padding': '0.5rem 1rem 1rem 1rem', 'height': '1000px' }}>
{props.content || 'Sidebar Content'}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
const PrimaryContent = (props) => (
<div
className="d-flex flex-fill border shadow"
style={{ 'margin': '8px', 'borderRadius': '5px' }}>
<h5 className="font-weight-bold" style={{ 'paddingLeft': '1rem' }}>
Primary Component
</h5>
</div>
);
const secondaryComponentWidth = {
square: 4,
rectangle: 8,
};
const SecondaryContent = (props) => (
<div
className="d-flex flex-fill"
style={{
'margin': '0rem 0.5rem 0.5rem 0.5rem',
'padding': '0.25rem 0.5rem 0.5rem 0.5rem',
'borderRadius': '5px',
'backgroundColor': `${props.color || 'LightSkyBlue'}`
}}>
{`Secondary Content Color: ${props.color}` || 'Default Secondary Content'}
</div>
);
const secondaryComponentColors = {
square: [
'LightCoral',
'LightPink',
'LightSalmon',
'MediumVioletRed',
'OrangeRed',
],
rectangle: [
'SeaGreen',
'MediumAquaMarine',
'DarkCyan',
'CadetBlue',
'PaleGreen',
],
};
const secondaryComponentOptions = _.reduce(
secondaryComponentColors,
(acc, colors, key) => {
acc[key] = _.map(
colors,
(color, index) => ({
heading: `${_.upperFirst(key)} ${index} Heading`,
content: <SecondaryContent color={color} />,
})
);
return acc;
},
{}
);
const SecondaryComponent = (props) => {
const colWidth = `col-sm-${props.width + 2} col-md-${props.width}`;
return (
<div
className={`col-xs-12 ${colWidth} border shadow position-relative`}
style={{
'borderRadius': '5px',
}}>
<div
className="row w-100 position-absolute"
style={{ 'overflow': 'hidden', 'height': '28px' }}>
<h5 className="font-weight-bold" style={{ 'paddingLeft': '1rem' }}>
{props.heading || 'Secondary Content Heading'}
</h5>
</div>
<div
className="row w-100 position-absolute"
style={{ 'top': '28px', 'height': 'calc(100% - 28px)' }}>
<div className="col d-flex flex-column">
<div className="row d-flex flex-fill" style={{ 'overflow': 'auto' }}>
{!!props.content ? props.content : <SecondaryContent />}
</div>
</div>
</div>
</div>
);
}
const SelectorSizeViz = (props) => (
<div className="row">
{_.map(
_.range(12),
(index) => (
<div
key={index}
className="col-1"
style={{
'padding': '1px',
'height': '50px',
'borderColor': 'LightGray',
'borderStyle': 'solid',
'borderWidth': `${
index === 0 || index === props.size
? '5px 0px 5px 5px'
: index === 11
? '5px 5px 5px 0px'
: '5px 0px 5px 0px'
}`,
'backgroundColor': `${index < props.size ? 'LightSlateGray' : ''}`
}}>
</div>
)
)}
</div>
);
class SelectorWorkflow extends Component {
constructor(props) {
super(props);
this.state = {
secondaryComponentType: '',
secondaryComponentTypeDisplay: '',
secondaryComponent: {},
enableComponentSelector: false,
enableComponentSetter: false,
};
this.setSecondaryComponentType = this.setSecondaryComponentType.bind(this);
this.setSecondaryComponent = this.setSecondaryComponent.bind(this);
this.setEnabledStatus = this.setEnabledStatus.bind(this);
this.clearSelection = this.clearSelection.bind(this);
}
setSecondaryComponentType(type) {
this.setState({
secondaryComponentType: type,
secondaryComponentTypeDisplay: type === 'square'
? 'Square Component'
: 'Rectangular Component',
});
}
setSecondaryComponent(secondaryComponent) {
this.setState({ secondaryComponent: secondaryComponent });
}
setEnabledStatus(statusUpdate) {
this.setState({ [statusUpdate]: true });
}
clearSelection() {
this.setState({
secondaryComponentType: '',
secondaryComponentTypeDisplay: '',
secondaryComponent: {},
enableComponentSelector: false,
enableComponentSetter: false,
});
}
render() {
return (
<div>
<div className="dropdown">
<button
className="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
{this.state.secondaryComponentTypeDisplay || 'Select Component Type'}
</button>
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
<div
className="dropdown-item"
style={{
'cursor': 'pointer',
'width': '225px',
}}
onClick={() => {
this.setSecondaryComponentType('square');
this.setEnabledStatus('enableComponentSelector');
}}>
<SelectorSizeViz size={secondaryComponentWidth['square']} />
</div>
{this.props.currentWidth <= 4
? <div
className="dropdown-item"
style={{
'cursor': 'pointer',
'width': '225px'
}}
onClick={() => {
this.setSecondaryComponentType('rectangle');
this.setEnabledStatus('enableComponentSelector');
}}>
<SelectorSizeViz size={secondaryComponentWidth['rectangle']} />
</div>
: null}
</div>
</div>
<div className="d-flex flex-fill" style={{ 'paddingTop': '5px' }}>
{this.state.enableComponentSelector
? <div className="dropdown">
<button
className="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
{this.state.secondaryComponent
&& this.state.secondaryComponent.heading
|| 'Select Component'}
</button>
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
{_.map(
secondaryComponentOptions[this.state.secondaryComponentType],
(componentOption, index) => (
<div
key={`${this.state.secondaryComponentType}_${index}`}
className="dropdown-item"
style={{ 'cursor': 'pointer' }}
onClick={() => {
this.setSecondaryComponent(componentOption);
this.setEnabledStatus('enableComponentSetter');
}}>
{componentOption.heading}
</div>
)
)}
</div>
</div>
: <div className="dropdown">
<button
className="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
disabled>
Select Component
</button>
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton"></div>
</div>}
</div>
<div className="d-flex flex-fill" style={{ 'paddingTop': '5px' }}>
{this.state.enableComponentSetter
? <div className="btn-group" role="group" aria-label="Sub-action buttons">
<button
type="button"
className="btn btn-success"
onClick={() => this.props.updateSecondaryComponents(
this.state.secondaryComponentType,
this.state.secondaryComponent
)}>
Add Component
</button>
<button
type="button"
className="btn btn-danger"
onClick={this.clearSelection}>
Clear
</button>
</div>
: <div className="btn-group" role="group" aria-label="Sub-action buttons">
<button type="button" className="btn btn-success" disabled>
Add Component
</button>
<button type="button" className="btn btn-danger" disabled>
Clear
</button>
</div>}
</div>
</div>
);
}
}
const SelectorContent = (props) => {
return (
<div style={{ 'padding': '0.25rem 0.5rem 0.5rem 1rem' }}>
<div className="d-flex flex-fill" style={{ 'paddingTop': '5px' }}>
<SelectorWorkflow {...props} />
</div>
</div>
);
}
const SecondaryComponentSelector = (props) => {
return <SecondaryComponent
width={secondaryComponentWidth['square']}
heading={'Add New Component'}
content={SelectorContent(props)} />;
}
const SecondaryComponentDeleter = (props) => (
<div className="position-fixed" style={{ 'bottom': '2px', 'left': '1px' }}>
<div className="dropup">
<button
className="btn btn-dark dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i className="fa fa-trash" aria-hidden="true"></i>
</button>
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
<div className="dropdown-item" style={{ 'width': '300px' }}>
<div className="row">
{_.map(
_.filter(props.widths),
(width, index, widths) => {
const isLast = index === widths.length - 1;
return (
<div
key={index}
className={`col-${width} delete-mask`}
style={{
'padding': '1px',
'height': '75px',
'borderColor': 'LightGray',
'borderStyle': 'solid',
'borderWidth': isLast ? '5px' : '5px 0px 5px 5px',
'backgroundColor': 'LightSlateGray',
'cursor': 'pointer',
}}
onClick={() => props.deleteSecondaryComponent(index)}>
</div>
);
}
)}
</div>
</div>
</div>
</div>
</div>
);
class VariableGrid extends Component {
constructor(props) {
super(props);
this.state = {
componentCount: 0,
secondaryComponents: [],
showSelector: false,
showDeleteOption: false,
};
this.updateSecondaryComponents = this.updateSecondaryComponents.bind(this);
this.deleteSecondaryComponent = this.deleteSecondaryComponent.bind(this);
}
updateSecondaryComponents(secondaryComponentType, secondaryComponent, components) {
let secondaryComponents = components || _.cloneDeep(this.state.secondaryComponents);
let componentCount = _.clone(this.state.componentCount);
let visibleComponents = [];
if (this.state.showSelector) {
// dropRight to exclude the selector component
secondaryComponents = _.dropRight(secondaryComponents);
}
if (!_.isEmpty(secondaryComponentType) && !_.isEmpty(secondaryComponent)) {
// Add new component passed in from params
componentCount = componentCount + 1;
visibleComponents.push({
componentType: secondaryComponentType,
component:
<SecondaryComponent
key={`secondary_${componentCount}`}
width={secondaryComponentWidth[secondaryComponentType]}
heading={secondaryComponent.heading}
content={secondaryComponent.content} />,
});
}
// incoporate other visible (non-selector) components
visibleComponents = _.flatMap(secondaryComponents.concat(visibleComponents));
const currentWidth = _.sumBy(
visibleComponents,
c => secondaryComponentWidth[c.componentType]
);
// a length of 8 or less means another component can be added (selector not counted)
const showSelector = currentWidth <= 8;
if (showSelector) {
componentCount = componentCount + 1;
visibleComponents.push({
componentType: 'square',
component:
<SecondaryComponentSelector
width={0}
currentWidth={currentWidth}
key={`secondary_${componentCount}`}
updateSecondaryComponents={this.updateSecondaryComponents} />,
});
}
this.setState({
componentCount: componentCount,
secondaryComponents: visibleComponents,
showSelector: showSelector,
showDeleteOption: currentWidth >= 4,
});
}
deleteSecondaryComponent(componentIndex) {
const currentComponents = _.cloneDeep(this.state.secondaryComponents);
this.updateSecondaryComponents(null, null, _.reject(
currentComponents,
(obj, index) => index === componentIndex
));
}
componentDidMount() {
this.updateSecondaryComponents();
}
render() {
return (
<div className="container-fluid h-100 d-flex flex-column">
<Header />
<div className="row d-flex flex-fill">
<div className="col-xs-12 col-sm-7 col-md-9 d-flex flex-column">
<div className="row d-flex flex-fill">
<PrimaryContent />
</div>
<div className="row d-flex flex-fill" style={{ 'padding': '0px 8px 8px 8px' }}>
{_.map(this.state.secondaryComponents, 'component')}
</div>
</div>
<div className="col-xs-12 col-sm-5 col-md-3 border-left d-flex flex-column">
<SidebarElement
heading={'Sidebar Heading 1'}
content={'Sidebar Content 1'} />
<SidebarElement
heading={'Sidebar Heading 2'}
content={'Sidebar Content 2'}
subActionBtns={true} />
</div>
</div>
{this.state.showDeleteOption
? <SecondaryComponentDeleter
widths={_.flatMap(this.state.secondaryComponents, 'component.props.width')}
deleteSecondaryComponent={this.deleteSecondaryComponent} />
: null}
</div>
);
}
}
ReactDOM.render(<VariableGrid />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.bundle.min.js"></script>
html, body, #root {
height: 100%;
}
.flex-fill {
flex: 1;
}
.shadow {
-moz-box-shadow: .05rem .1rem .15rem LightGray;
-webkit-box-shadow: .05rem .1rem .15rem LightGray;
box-shadow: .05rem .1rem .15rem LightGray;
}
.delete-mask:hover {
background-color: DarkSlateGray !important;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment