Custom layouts
You can create custom layouts using Jutro components. Benefits include:
- Separation of concerns - metadata can be focused on content only, not mixed with layout definitions
- Reuse of layouts between different pages
- Easier to make changes both to content and layout
- Documenting and naming layouts used in the application
On this page, we show you how to use GridLayout for building custom layouts.
Creating a layout
There are many different techniques to achieve the desired result. You can use GridItems, GridLayout props, nest layouts, or create separate layouts for each breakpoint.
GridItems
A component position and behavior can be modified by wrapping it in a GridItem component. For example, to make one of the components two columns wide you can write:
<GridLayout
columns={[1, 1]}
id="grid-layout">
<SomeElement />
<SomeElement />
<GridItem colSpan={2}>
<SomeElement />
</GridItem>
</GridLayout>
GridLayout automatically wraps children in GridItems, so you can define all properties of GridItems in a single place by passing props to GridLayout.
<GridLayout
columns={[1, 1]}
gridItems={[null, null, { colSpan: 2 }]}
id="grid-layout">
<SomeElement />
<SomeElement />
<SomeElement /> // will be 2-column wide
</GridLayout>
GridItem passes a style
prop to a wrapped component. The component then has to pass it to the underlying HTML tag. If it doesn't do that (some Jutro components don't), try passing clone={false}
to GridItem:
<GridItem
colSpan={2}
clone={false}>
<SomeElement />
</GridItem>
Then SomeElement will be wrapped in an extra <div>
that is styled instead.
Check the documentation page for grid items for more information.
Nesting GridLayouts
You can put GridLayouts inside another GridLayout to implement more complex designs:
<GridLayout
columns={[1, 1]}
id="grid-layout">
<GridLayout id="column-1">
<SomeElement />
<SomeElement />
</GridLayout>
<GridLayout id="column-2">
<SomeElement />
<SomeElement />
</GridLayout>
</GridLayout>
Using breakpoints
There are special props that can be used to change layout on different screen sizes: tablet
, phoneWide
, and phone
. Properties passed to these props are applied on specific breakpoints.
To change the number of columns on different screen sizes you can use:
<GridLayout
columns={[1, 1, 1, 1]}
tablet={{ columns: [1, 1, 1] }}
phoneWide={{ columns: [1, 1] }}
phone={{ columns: [1] }}
id="grid-layout">
<SomeElement />
<SomeElement />
<SomeElement />
</GridLayout>
You could also customize everything based on the current breakpoint. This technique, however, is not recommended. Do not use conditionals with a breakpoint value.
const SomeLayout = (props) => {
const { breakpoint, breakpointProps } = useBreakpoint(props);
// This is wrong, do not try this!
if (breakpoint === 'tablet') {
return <GridLayout>/*tablet-specific layout*/</GridLayout>;
} else if (breakpoint === 'phoneWide') {
return <GridLayout>/*wide-phone-specific layout*/</GridLayout>;
} else if (breakpoint === 'phone') {
return <GridLayout>/*phone-specific layout*/</GridLayout>;
} else {
return <ColumnLayout>/*default layout*/</ColumnLayout>;
}
};
On a side note: useBreakpoint
also returns breakpointProps
with overrides from tablet
, phoneWide
and phone
applied to normal props. This is how some Jutro components are implemented.
Passing children to a layout
To make a layout component reusable, you need a way for passing child components.
Using order of children
A simple technique is to differentiate children based on their order, for example: "the first child is always two columns wide, the fourth is placed in the third column". It can be used when the layout is simple, and ideally the number of children is known.
const SomeLayout = (props) => {
const { children } = props;
return (
<GridLayout
gridItems={[{ colSpan: 2 }, null, null, { colStart: 3 }]}
id="grid-layout">
{children}
</GridLayout>
);
};
// usage:
<SomeLayout>
<SomeComponent /> // will be 2 columns wide
<SomeComponent />
<SomeComponent />
<SomeComponent /> // will be in the third column
</SomeLayout>;
Using the slot
prop in children
To make placement of children explicit and independent of their order, you can add a special prop (slot
) to all children of the layout. Please note that the name of the prop is not treated in any special way by Jutro.
import { groupBy } from 'lodash';
const SomeLayout = (props) => {
const { children } = props;
const childArray = React.Children.toArray(children);
const childrenBySlot = groupBy(childArray, (child) => child.props.slot);
// alternatively:
// const leftColumn = childArray.filter((child) => child.props.slot === "left");
// const rightColumn = childArray.filter((child) => child.props.slot === "right");
return (
<GridLayout
columns={[1, 1]}
id="grid-layout">
<GridLayout id="left-column">{childrenBySlot['left']}</GridLayout>
<GridLayout id="right-column">{childrenBySlot['right']}</GridLayout>
{childrenBySlot[undefined]}
</GridLayout>
);
};
// usage:
<SomeLayout>
<SomeComponent slot="left" />
<SomeComponent slot="left" />
<SomeComponent slot="right" />
<SomeComponent slot="left" />
<SomeComponent />
<SomeComponent slot="invalidSlot" /> // won't be rendered at all
</SomeLayout>;
In the example above, children
are split based on the slot
property into a few arrays, which are then used in the layout. Children with unknown slot
are not rendered in this example. The order of children with the same slot prop matters, but children with different slots can be mixed with each other.
You can decide to use semantic names for slots like "important"
, "navigation"
, "informational"
, so for example columns can be reordered later without changes in usage.
Using a wrapper component
If you cannot use the slot
prop with your child components, you can use a wrapper component. Add a simple component that passes children through, and wrap each child with it.
For example, the slot
prop might not work when you are using Typescript but you do not want to suppress the "Unknown property" errors, or when the child components are already using the slot
prop for a different purpose.
Implementation of the SomeLayout
component is the same as in the previous example.
const SomeLayoutItem = ({ children, slot }) => <>{children}</>;
// usage:
<SomeLayout>
<SomeLayoutItem slot="left">
<SomeComponent />
</SomeLayoutItem>
<SomeLayoutItem slot="right">
<SomeComponent />
</SomeLayoutItem>
</SomeLayout>;
Using render props
As an alternative to using the slot
property, you can use render props to render children. This is not recommended, because it is harder to use with metadata.
const SomeLayout = (props) => {
const { children, renderLeftColumn, renderRightColumn } = props;
return (
<GridLayout
columns={[1, 1]}
id="grid-layout">
<GridLayout id="left-column">{renderLeftColumn()}</GridLayout>
<GridLayout id="right-column">{renderRightColumn()}</GridLayout>
</GridLayout>
);
};
// usage:
<SomeLayout
renderLeftColumn={() => (
<>
<SomeComponent />
<SomeComponent />
<SomeComponent />
</>
)}
renderRightColumn={() => <SomeComponent />}
/>;
Tips & tricks
Breakpoints in CSS
Breakpoints are accessible in CSS to further customize the layout. For example, you can remove a padding on phones:
@import '~@jutro/theme/assets/sass/helpers';
.someLayout {
padding: var(--GW-LAYOUT-6);
@include gw-breakpoint-down(phone) {
padding: 0;
}
}
Pass ID through props
If the layout component appears on a page many times, you have to assign different ids each time it's used. Don't forget to assign an ID to every GridLayout, Grid and ColumnLayout used. Other examples on this page are not passing the ID only for clarity.
const SomeLayout = (props) => {
const { id } = props;
return <GridLayout id={id} />;
};
// usage:
return (
<SomePage>
<SomeLayout id="form-1" />
<SomeLayout id="other-form" />
</SomePage>
);
Use simple layout components
If you don't use any of the techniques from this document, it is still acceptable and recommended to make small components like this one:
const SimpleLayout = (props) => {
const { id, children } = props;
return (
<GridLayout
id={id}
columns={['1fr', '70px']}>
{children}
</GridLayout>
);
};
Was this page helpful?