Skip to content

Latest commit

 

History

History
911 lines (674 loc) · 30.1 KB

README.md

File metadata and controls

911 lines (674 loc) · 30.1 KB

React FluidGrid Component

npm package gzip-size install-size build coverage license donate

React FluidGrid creates a "responsive grid" layout. FluidGrid was inspired by Material-UI Grid ("MUI Grid"). It emulates the syntax and props of MUI Grid as closely as possible so devs familiar with that can learn FluidGrid quickly.

Motivation

FluidGrid was created for use-cases that MUI Grid could not handle well. Once I created it, I realized that it handles most layouts easier, even those that MUI Grid could handle as well.

The initial use-case was a container filled with 'cards' that required minimum widths to display correctly. This container was surrounded on both sides by collapsible sidebars, plus a lot of responsive configuration in media queries. Since MUI Grid can react only to the full screen-width, it could not handle this complex layout automatically.

We tried making it work by adding custom logic, but all the variables made it very complicated. It was also brittle because any changes to other elements could break the grid sizing logic.

The problem with ALL "grids" is that they rely on spanning columns of a virtual grid. This means that the column-span values must be changed in order to reflow the grid items. The developer is responsible for figuring out all the math and breakpoints required to make things work decently at all resolutions.

How is FluidGrid different?

FluidGrid does NOT rely on an imaginary grid with imaginary column. It's really a fluid-flow layout rather than a fluid-grid, but I named it FluidGrid because it emulates a grid layout, and can be used in place of them. It's structure and props also deliberately emulate Material-UI Grid.

Like MUI Grid, FluidGrid used flexbox to create the layout. Unlike MUI Grid, it does not use media-queries, so it is not reliant on "screen width".

FluidGrid generates 100% static CSS. It does not manipulate or modify the grid once it is rendered. Responsiveness is controlled solely by the CSS, which makes it FAST. If you expand or collapse a sidebar, the grid will immediately reflow to suit the new container size.

NOTE: FluidGrid does NOT use or require Material UI.

How does FluidGrid work?

Configuring a FluidGrid is as simple as adding props like minWidth. Any valid CSS 'length' values work, including percentages. FluidGrid has a rich API so it can be customized to handle almost any layout.

Because FluidGrid is based on cell-content width instead of screen-width, it cares only about the width of the grid container. If you expand or collapse a sidebar, the grid will immediately reflow to suit the resized container. No media queries, special state-classes, or re-rendering is necessary.

Adding item spacing and divider rules is as simple as adding props; for example:

<FluidGrid 
    container 
    spacing="20px"
    columnDivider={{ width: '2px', color: '#669' }}
>
    <FluidGrid item minWidth="320px">
        <CustomCardOne />
    </FluidGrid>

    <FluidGrid item minWidth="320px">
        <CustomCardTwo />
    </FluidGrid>

    <FluidGrid item minWidth="320px">
        <CustomCardThree />
    </FluidGrid>

    <FluidGrid item minWidth="600px">
        <CustomCardFour />
    </FluidGrid>
</FluidGrid>

The code above means no card will ever be less than 300px wide (unless the grid itself is), and the cards will be spaced 20px apart both vertically and horizontally, but with no space at the top or sides of the grid. As many cards as possible will be shown on each 'row'. Each card will grow proportionally to fill the grid-row. The options shown could be enhanced to make the sizing more refined.

Grid Structure

A FluidGrid consists of an outer-wrapper (Container), and inner-wrapper, plus any number of grid-cells (Items). The content of each grid-cell is wrapped by FluidGrid Item element. Only the FluidGrid wrappers have CSS applied. The contents of the grid are never touched.

By default, a DIV is used for both the Container and Item wrappers. However any element type can be specified, including a React component. In some scenarios, this can eliminate unnecessary element nesting. For example, instead of putting a MUI Typography component inside a FluidGrid Item DIV, the Typography component can be the Item component. This example shows both structures.

<FluidGrid container>
    <FluidGrid item minWidth="33%">
        <Typography variant="subheading">
            Text Content
        </Typography>
    </FluidGrid>

    <FluidGrid item minWidth="33%" component="Typography" variant="subheading">
        Text Content
    </FluidGrid>
    
    ...
</FluidGrid>

Basic Grid Properties

Both the Container and Item wrappers are created using the same FluidGrid component. This follows the style of the MUI Grid component. The syntax is:

// Grid Container syntax - default is to be a container
<FluidGrid spacing="20px">
<FluidGrid container spacing="20px">

// Grid Item syntax
<FluidGrid item minWidth="400px">

Customizing the Grid

A FluidGrid can have vertical and/or horizontal spacing between grid-cells. It can also generate divider-borders between rows and/or columns. These borders are customizable using standard CSS border rules.

Additional flexbox CSS rules can also be added to customize both the flex container and the flex items. Use normal CSS-in-Javascript syntax to add styles to the grid configuration. The component will calculate and generate the final CSS to achieve the 'goal' specified. This means the generated CSS may not be the same as the props passed in!

See the FluidGrid API section below.

Responsive FluidGrid Sample

The sample below is from the "Mixed Width Columns" demo available at: allpro.github.io/react-fluid-grid
(Cosmetic props and styles removed for brevity.)

Mixed-Width Sample Code (click to show)
 
import React, { Fragment } from 'react'
import Typography from '@material-ui/core/Typography'
import FluidGrid from '@allpro/react-fluid-grid'

// Verbose config options as a sample; often just `spacing` is needed
const gridConfig = {
  justify: 'flex-start',
  alignContent: 'stretch',
  alignItems: 'stretch',

  columnSpacing: 16,
  rowSpacing: 32,
  rowDivider: {
    width: 1,
    color: 'blue',
  },
}

function ReactFluidGridExample() {
  return (
    <FluidGrid container {...gridConfig}>

      <FluidGrid item flexBasis="400px" minWidth="50%" flexGrow={99}>
        <Typography>{'flexBasis="400px" minWidth="50%"'}</Typography>
      </FluidGrid>

      <FluidGrid item flexBasis="400px" minWidth="50%" flexGrow={99}>
        <Typography>{'flexBasis="400px" minWidth="50%"'}</Typography>
      </FluidGrid>

      <FluidGrid item flexBasis="250px" minWidth="33%">
        <Typography>{'flexBasis="250px" minWidth="33%"'}</Typography>
      </FluidGrid>

      <FluidGrid item flexBasis="250px" minWidth="33%">
        <Typography>{'flexBasis="250px" minWidth="33%"'}</Typography>
      </FluidGrid>

      <FluidGrid item flexBasis="250px" minWidth="33%">
        <Typography>{'flexBasis="250px" minWidth="33%"'}</Typography>
      </FluidGrid>

      <FluidGrid item minWidth="150px" flexGrow={99}>
        <Typography>{'minWidth="150px"'}</Typography>
      </FluidGrid>

      <FluidGrid item minWidth="150px" flexGrow={99}>
        <Typography>{'minWidth="150px"'}</Typography>
      </FluidGrid>

      <FluidGrid item minWidth="150px" flexGrow={99}>
        <Typography>{'minWidth="150px"'}</Typography>
      </FluidGrid>

      <FluidGrid item minWidth="150px" flexGrow={99}>
        <Typography>{'minWidth="150px"'}</Typography>
      </FluidGrid>

    </FluidGrid>
  )
}

export default ReactFluidGridExample

Live Demos

Try the demos at: https://allpro.github.io/react-fluid-grid

Play with the demo code at: https://codesandbox.io/s/github/allpro/react-fluid-grid/tree/master/example

If you pull or fork the repo, you can run the demo like this:

  • In the root folder, run npm start
  • In a second terminal, in the /example folder, run npm start
  • The demo will start at http://localhost:3000
  • Changes to the component or the demo will auto-update the browser

Installation

  • NPM: npm install @allpro/react-fluid-grid
  • Yarn: yarn add @allpro/react-fluid-grid
  • CDN: Exposed global is FormManager
    • Unpkg: <script src="https://unpkg.com/@allpro/react-fluid-grid/umd/@allpro/react-fluid-grid.min.js"></script>
    • JSDelivr: <script src="https://cdn.jsdelivr.net/npm/@allpro/react-fluid-grid/umd/@allpro/react-fluid-grid.min.js"></script>

FluidGrid API

FluidGrid has 2 components, which are differentiated by setting either a container or item prop.

  • container   {boolean} [true]
    Sets component to be a 'FluidGrid Container'

  • item   {boolean} [false]
    Sets component to be a 'FluidGrid Item'

Sample of container and item use to differentiate component mode.

<FluidGrid container spacing={24}>
    <FluidGrid item minWidth="300px">
        <ContentsOne />
    </FluidGrid>

    <FluidGrid item minWidth="300px">
        <ContentsTwo />
    </FluidGrid>
</FluidGrid>

The props for each component-mode are categorized for clarity. All props are optional, but if no props are specified, the generated output will not be useful!

FluidGrid container Props

Special Props (expand)

    These props are unique to the FluidGrid container .

  • component   {Component|string} ["div"]
    The wrapper-element generated by FluidGrid.

  • containerOverflow   {string} [""]
    Value must be a valid CSS measurement, like "4px" or "1em"
    This increases the container size to contain visual effects that extend beyond the boundary of items, like a drop-shadow or glow effect.

  • columnSpacing   {integer|string} [0]
    Horizontal spacing between items
    Value must be a valid CSS measurement, like "4px" or "1em"
    See Spacing and Divider Logic

  • rowSpacing   {integer|string} [0]
    Vertical spacing between items
    Value must be a valid CSS measurement, like "4px" or "1em"
    See Spacing and Divider Logic

  • spacing   {integer|string} [0]
    Horizontal and Vertical spacing between items.
    Value must be a valid CSS measurement, like "4px" or "1em"
    Is overridden by columnSpacing or rowSpacing props.
    See Spacing and Divider Logic

  • columnDivider   {object} [{ width: 0, style: 'solid', color: '#CFCFCF' }]
    CSS attributes for border to display between columns:

    • width MUST be an integer, representing a pixel value
    • style can be any valid CSS border-style value
    • color can be any valid CSS color value
    • Any key other than these 3 will be ignored
    • Unset keys will use the default values shown
  • rowDivider   {object} [{ style: 'solid' color: '#CFCFCF' }]
    CSS attributes for border to display between rows
    Same attributes as shown above for columnDivider
    Also see Spacing and Divider Logic

Extra Flexbox Props (expand)

    These props are not used for grid logic, but FluidGrid does set logical defaults for them. You can use these props to override these defaults.

  • justifyContent or justify   {string} ["flex-start"]
    Flexbox rule for grid-container.
    The justify alias is for consistency with Material-UI Grid.

  • alignContent   {string} ["stretch"]
    Flexbox rule for grid-container.

  • alignItems   {string} ["stretch"]
    Flexbox rule for grid-container.

Default Flexbox-Item Props (expand)

    Flexbox-logic props for grid-items can be set on the grid-container as 'defaults' for all items. These defaults can be overridden on individual grid-items.

Styling Props (expand)
  • className   {string} [null]
    Class to apply to grid-container.

  • style   {object} [null]
    Styles that will be assigned to the grid-container.

FluidGrid item Props

Special Props (expand)

    These props are unique to FluidGrid items.

  • component   {Component|string} ["div"]
    The wrapper-element for the grid-item generated by FluidGrid.
Flexbox-Logic Props (expand)

    These props are used as inputs to FluidGrid logic for the final CSS. They may or may not translate directly to a CSS rule. For example, setting minWidth="300px" can generate CSS like flex: 1 0 300px;, depending on what other props are set.

  • flexGrow   {integer} [null]
    Flexbox logic prop for grid-item.

  • flexShrink   {integer} [null]
    Flexbox logic prop for grid-item.

  • flexBasis   {string} [""]
    Flexbox logic prop for grid-item.

  • minWidth   {string} [""]
    Flexbox logic prop for grid-item.

  • maxWidth   {string} [""]
    Flexbox logic prop for grid-item.

Styling Props (expand)
  • className   {string} [null]
    Class to apply to grid-item wrapper.

  • style   {object} [null]
    Styles that will be assigned to the grid-item wrapper.

Spacing and Divider Logic

FluidGrid items can have 'spacing' between them, both vertically and horizontally. Row and column spacing can be different. The same applies for 'divider rules' between columns and/or rows in the grid.

FluidGrid adds spacing only between items. There is never any spacing on the top, bottom, or sides of the grid. If you want outer spacing, add margins to the 'container' element using a 'style' or 'class' prop.

The width of Divider rules are in addition to any spacing specified. Dividers are displayed in the middle of the spacing. For example:

<FluidGrid
  container
  columnSpacing="10px"
  columnDivider={{ width: '4px', color: 'red' }}
>

These props result in items spaced a total of 14px apart, (10px-spacing + 4px-divider). The spacing and divider are distributed like this:

  • Item #1 – right-edge
  • 5px spacing (1/2 of 10px)
  • 4px divider
  • 5px spacing (1/2 of 10px)
  • Item #2 – left-edge

Effect of spacing on width props

When you specify a minWidth or flexBasis props, it affects the width of the FluidGrid Item-wrapper. Spacing also must be taken into account because it too is inside the Item-wrapper. It's not possible to perfectly calculate the inner-width of item-wrappers, due to how FluidGrid generates the layout. However FluidGrid is still far more accurate than a standard grid.

As a general guideline, you should add the spacing to the desired min-width of your component. However there is no spacing at either end of each row, so this is only an approximation.

Example

Goals for this example:

  • <MyCard> components should always be at least 300px wide
  • Have 30px spacing between each card
  • Have a 2px divider-rule, (at the center of the item spacing)

To achieve this, add the 30px spacing to the 300px 'card' min-width. This will provide slightly more than 300px card-width because grid-items at the start and end of each row only have spacing on the inside. We will ignore the extra spacing created by the 2px divider-rule.

<FluidGrid container
  columnSpacing="30px"
  columnDivider={{ width: '2px' }}
  minWidth="330px" // DEFAULT item minWidth
>
  <FluidGrid item><MyCard id="1" /></FluidGrid>
  <FluidGrid item><MyCard id="2" /></FluidGrid>
  <FluidGrid item><MyCard id="3" /></FluidGrid>
</FluidGrid>
3 cards per row

At least 990px width is needed to fit 3 cards (3 x 330px). This table shows widths when container is exactly 990px wide:

1st Row

Width Item
308.67px MyCard #1
15px spacing (1/2 of 30px)
2px divider-rule
15px spacing (1/2 of 30px)
308.67px MyCard #2
15px spacing (1/2 of 30px)
2px divider-rule
15px spacing (1/2 of 30px)
308.67px MyCard #3
990px Container Width

NOTE that the cards are slightly more than 300px wide.

2 cards per row

At least 660px width is needed to fit 2 cards (2 x 330px). This table shows widths when container is exactly 660px wide:

1st Row

Width Item
314px MyCard #1
15px spacing (1/2 of 30px)
2px divider-rule
15px spacing (1/2 of 30px)
314px MyCard #2
660px Container Width

2nd Row

Width Item
660px MyCard #3
660px Container Width

NOTE that 1st-row cards are even wider because there are fewer across, which means fewer 'spaces' between cards.

1 card per row

When container is less than 660px width, only 1 card can fit per row. This table shows widths when container is 659px wide:

1st Row

Width Item
659px MyCard #1
659px Container Width

2nd Row

Width Item
659px MyCard #2
659px Container Width

3rd Row

Width Item
659px MyCard #3
659px Container Width
Layout Reflow

As the last example shows, when the container becomes just 1px narrower than the sum of item minWidths, the grid will reflow all items. It does not matter why the container width changed!

FluidGrid reflow is more automatic and precise because it is NOT reliant on screen-width breakpoints. If there is an expandable sidebar that affects container width, this will trigger a reflow the same as changing the window size. All that matters is the container width.

FluidGrid is especially helpful in apps with multiple grid layouts, with varying content width requirements. It's very difficult to set media-query breakpoints that work optimally in such scenarios, if you care about the minimum-width of items.

Layout Tips & Tricks

This section provides tips for refining your layout, including advanced use of flexbox rules.

Using flexBasis + minWidth

Setting either flexBasis or minWidth props will generate identical CSS for a FluidGrid item. Both these rules affect 'min-width', so FluidGrid considers them equivalent. However, setting both flexBasis and minWidth props is different.

It can be useful to combine percentage widths with pixel-widths to prevent packing many small componenets into a row when the container is very wide, like on a high-resolution monitor.

Assume these goals for a grid:

  • Cards should always be at least 250px wide
  • Should have 24px spacing between each item
  • Should show 3-items per row MAX, regardless of container width

To achieve this, set both pixel and percentage min-widths. As suggested in Effect of spacing on width-props, we'll add the spacing to our desired inner-width: 250 + 24 = 274px:

<FluidGrid container
  columnSpacing="36px"
  columnDivider={{ width: '2px' }}
  flexBasis="274px" // DEFAULT item flexBasis
  minWidth="33%"    // DEFAULT item minWidth
>
  <FluidGrid item><MyCard id="1" /></FluidGrid>
  <FluidGrid item><MyCard id="2" /></FluidGrid>
  <FluidGrid item><MyCard id="3" /></FluidGrid>
  <FluidGrid item><MyCard id="4" /></FluidGrid>
  <FluidGrid item><MyCard id="5" /></FluidGrid>
  <FluidGrid item><MyCard id="6" /></FluidGrid>
</FluidGrid>

Setting flexBasis="448px" provides our absolute min-width, (unless container-width is less). The minWidth="33%" becomes a secondary min-width. This means no more than 3 cards can ever appear on a single row.

Without minWidth="33%", a container 1400px wide would display 5 items-per-row (5 x 274 (min) = 1370px). This may look more crowded than is desired. Minimum width is not always the same as optimal width!

Maintain Consistent Column Widths

A grid of equal-width items usually should not have items on the last row that span multiple 'columns'. Instead, the width of last-row items should be consistent with those above. After the last item, remaining columns should display as 'empty'. If vertical dividers are displayed, these should all display equally.

An old CSS trick for such scenarios is to use 'dummy items' to fill gaps in the last row of wrapped items. This is easily achieved by adding empty FluidGrid items. The placeholder flag should be passed so FluidGrid does not pad these.

This example is similar to one above, but with 2 placeholder-items added:

<FluidGrid container
  columnSpacing="36px"
  flexBasis="274px" // DEFAULT item flexBasis
  minWidth="33%"    // DEFAULT item minWidth
>
  <FluidGrid item><MyCard id="1" /></FluidGrid>
  <FluidGrid item><MyCard id="2" /></FluidGrid>
  <FluidGrid item><MyCard id="3" /></FluidGrid>
  <FluidGrid item><MyCard id="4" /></FluidGrid>
  <FluidGrid item placeholder />
  <FluidGrid item placeholder />
</FluidGrid>

The minWidth="33%" prop means there will never be more than 3 items per row. If the container is wide enough, the 1st row contains 3 cards, leaving only MyCard #4 in the 2nd row. By default, MyCard #4 would stretch to fill the entire 2nd row.

The 2 placeholder-items prevent this. There are now 3 items on the 2nd row, so MyCard #4 will be the same width as MyCard #1 above it. The column-divider rules will also extend between these placeholders, creating a more refined look. (This can be changed though.)

When using placeholders, add enough to handle the maximum number of 'blank columns' your grid may ever have at the end.
It's OK to have extra placeholders — unneeded ones are invisible.

Control Item Expansion

By default, every item will stretch to fill each row. All items grow proportionally — as flexbox specifies. This behaviour can be changed by adding a flexGrow prop.

By default, FluidGrid applies flexGrow: 1 to all items, (unless a maxWidth is set). This causes all item items to grow/stretch equally. The flexGrow values affect proportionality, so an item with flexGrow={2} will grow beyond its min-width at twice the rate of the default. For example:

<FluidGrid container
  columnSpacing="0"
  minWidth="300px" // DEFAULT item minWidth
  style={{ width: '1200px' }}
>
  <FluidGrid item><CardA /></FluidGrid>
  <FluidGrid item><CardB flexGrow={3} /></FluidGrid>
  <FluidGrid item><CardC flexGrow={6} /></FluidGrid>
</FluidGrid>

Without the flexGrow props shown, all items would stretch equally, so each would become 400px wide to fill the 1200px container-width.

However with the flexGrow props shown above:

  • CardB will grow 3x as fast as CardA
  • CardC will grow 2x as fast as CardB; and 6x as fast as CardA

This is how the grid would auto-size items:

Width Item (minWidth + Growth)
330px CardA (300 + 30)
390px CardB (300 + 90)
480px CardC (300 + 180)
1200px Container Width

What if you want some cards to not grow at all, — except when necessary to fill the grid? Instead, other cards should grow to fill the row. This can be achieved by using large flexGrow numbers, like this:

<FluidGrid container columnSpacing="30px">
  <FluidGrid item minWidth="240px">
  	<CardA_Small />
  </FluidGrid>

  <FluidGrid item minWidth="500px" flexGrow={99}>
  	<CardB_Large />
  </FluidGrid>

  <FluidGrid item minWidth="300px">
  	<CardC_Small />
  </FluidGrid>

  <FluidGrid item minWidth="600px" flexGrow={99}>
  	<CardD_Large />
  </FluidGrid>
</FluidGrid>

If the container is only 700px wide, CardA_Small will grow to fill row-1, because there is not enough room to also fit CardB_Large.

If the container is 900px wide, both CardA_Small and CardB_Large will fit on row-1, with 160px of extra room. Because CardB_Large has flexGrow={99}, it will grow 99-times faster than CardA_Small. This means CardA_Small will stretch just 1.6px (1% of 160px), while CardB_Large will stretch 158.4px (99% of 160px).

Normally most items in a grid should be able to stretch, or else they cannot fill the row when the container is narrow, like on mobile. If you want -0- stretching, add a maxWidth prop like minWidth="240px" maxWidth="240px". However, this prevent any stretching even when the item is on a row all by itself, which will leave gaps in the grid.

Max-Width and Flex-Shrink

FluidGrid logic also handles props for maxWidth and flexShrink. These work exactly as you'd expect them to. This Readme does not include examples for these because they are not commonly needed for grid layouts.

Justification and Alignment Props

FluidGrid applies reasonable flexbox defaults to container elements so that items stretch both horizontally and vertically. This provides a grid-like appearance. You can override these defaults at the container level — see the API.

Other Flexbox props

FluidGrid is primarily concerned with props that affect its calculations. If you need additional flexbox rules for FluidGrid items, like alignSelf, use a style or class prop to add them normally.

Be careful to not apply styles that conflict with those auto-generated by FluidGrid. For example, do not apply margins to items as it may cause unexpected results.

Media-Query Integration

FluidGrid is not designed to work directly with media-queries. However, if using a helper that provides breakpoint 'props' — like the Material-UI withWidth() HOC — then you can use this to dynamically calculate FluidGrid props. This is not normally necessary, but is simple to do:

import withWidth, { isWidthUp } from '@material-ui/core/withWidth'

const minWidth = isWidthUp('md', this.props.width) ? '400px' : '300px'

return (
  <FluidGrid container>
    <FluidGrid item minWidth={minWidth}><MyCardA /></FluidGrid>
    <FluidGrid item minWidth={minWidth}><MyCardB /></FluidGrid>
    <FluidGrid item minWidth="600px"><MyCardC /></FluidGrid>
  </FluidGrid>
)

Unexpected or Inadequate Results

If you use FluidGrid and find a usecase it can't handle well, create a sample and share it with me. If practical, I'll update FluidGrid to better address it.


Built With

Contributing

Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository.

License

MIT © allpro
See LICENSE file for details