A Flexbox grid builder field
First, install the plugin with its peer dependencies:
npm i react-tinacms-field-grid react react-dom tinacms react-tinacms-inline
This field assumes you alread have Tina setup in a React application.
If not, please see the Tina Documentation to get started.
First, setup a Tina form inside an InlineForm:
import { useForm } from "tinacms";
import { InlineForm } from "react-tinacms-inline";
export function MyApp(props) {
const form = useForm(props);
return (
<InlineForm form={form}>
</InlineForm>
);
}
Now, you need to add your InlineGrid
, which requires:
- A row block and a column block
- A set of UI blocks to render in the grid (these can be any blocks compatible with the
InlineBlocks
component). - A
name
, specifying where theInlineGrid
's data is stored on the form
First, let's create the row and column blocks.
import { BlocksControls } from 'react-tinacms-inline'
export const RowBlock = ({index, children}) => (
<BlocksControls index={props.index}>
<div style={{display: "flex"}}>
{children}
</div>
</BlocksControls>
)
export const RowBlockTemplate = {
label: 'Row',
defaultItem: {
columns: []
}
fields: []
}
export const RowBlockPreview = () => <RowBlock>This is a row</RowBlock>;
export default {
Component: RowBlock,
template: RowBlockTemplate,
preview: RowBlockPreview
}
export const ColumnBlock = ({index, children}) => (
<div style={{display: "flex"}}>
<BlocksControls index={props.index}>
{children}
</BlocksControls>
</div>
)
export const ColumnBlockTemplate = {
label: 'Column',
defaultItem: {
blocks: []
}
fields: []
}
export const ColumnBlockPreview = () => <ColumnBlock>This is a column</ColumnBlock>;
export default {
Component: RowBlock,
template: RowBlockTemplate,
preview: RowBlockPreview
}
Now, let's create a paragraph block to use in the grid.
export const ParagraphBlock = ({index, text}) => (
<p>
{children}
</p>
)
export const ParagraphBlockTemplate = {
label: 'Paragraph',
defaultItem: {
text: 'Hello world!',
},
fields: [
{ name: 'text', component: 'textarea', label: "Paragraph text"}
]
}
export const ParagraphBlockPreview = () => <ParagraphBlock text="This is a column" />;
export default {
Component: ParagraphBlock,
template: ParagraphBlockTemplate,
preview: ParagraphBlockPreview
}
import { useForm } from "tinacms";
import { InlineForm } from "react-tinacms-inline";
import { InlineGrid } from "react-tinacms-field-grid";
import RowBlock from "./RowBlock";
import ColumnBlock from "./ColumnBlock";
import ParagraphBlock from "./ParagraphBlock";
export function MyApp(props) {
const form = useForm(props);
const blocks = useMemo(() => {
return {
paragraph: ParagraphBlock
}
}, []);
return (
<InlineForm form={form}>
<InlineGrid
row={RowBlock}
column={ColumnBlock}
blocks={blocks}
/>
</InlineForm>
);
}
Now, open up your app and see the result:
- Add a row
- Add a column
- Add a paragraph
- Click the "settings" icon on the paragraph to edit the text!
To add configuration for your row or column, such as setting how many column lengths a give column should span, you can update the template.
For example, to add a "colspan" attribute that updates the flex properties of the column, we'll first update the defaultItem
of the column template:
export const ColumnBlockTemplate = {
label: 'Column',
defaultItem: {
blocks: []
colspan: 4
}
fields: []
}
Then we'll update the fields
option of the column template to allow the colspan to be changed:
export const ColumnBlockTemplate = {
label: 'Column',
defaultItem: {
blocks: []
colspan: 4
}
fields: [
{ name: 'colspan', component: 'number', label: "Column span" }
]
}
Lastly, we'll update the actual component to use the value by:
- Passing in colspan via the
data
prop passed to the block, defaulting to4
(one third) if not provided - Creating a function to take a column span (1-12) to a percentage width
- Set the
flex-basis
andmax-width
styles of the column
export function ColumnBlock = ({index, data, children}) {
const colspan = data.colspan ?? 3;
const round = (value, decimals) => Number(Math.round(value+'e'+decimals)+'e-'+decimals);
const width = round(100 / colspan, 2) * 100;
return (
<div style={{flexBasis: width + "%", maxWidth: width + "%"}}>
<BlocksControls index={props.index}>
{children}
</BlocksControls>
</div>
)
)
Now columns have a settings modal to configure their column span.
If you want new rows to default to having a certain number of columns, we can update the rows template.
For example, to default to three columns in every row, add 3 columns to the row template's defaultItem
.
export const RowBlockTemplate = {
label: 'Row',
defaultItem: {
columns: [
{
_template: "column",
blocks: []
},
{
_template: "column",
blocks: []
},
{
_template: "column",
blocks: []
}
]
}
fields: []
}
If you want new columns to default to having a certain set of blocks, we can update the columns template.
For example, if we wanted every new column to have a paragraph when created:
export const ColumnBlockTemplate = {
label: 'Column',
defaultItem: {
blocks: [
{
_template: "paragraph",
text: "Hello world, I'm a default paragraph!"
}
]
}
fields: []
}
For a simple InlineGrid
with a few blocks, performance is not a problem. But once your get to dozens or even hundreds of blocks, this becomes a major performance concern for users.
Considering, InlineGrid
allows the lazy loading of blocks, and will:
- Lazy load all available blocks when in editing mode
- Lazy load only the blocks used by the layout when not in editing mode
This is done by passing an array of block resolvers, which match the interface:
{
id: string;
importFunc: () => {
Component: React.FunctionalComponent
template: BlockTemplate
preview: React.FunctionalComponent
}
}
For example, for our example above:
export function MyApp(props) {
const form = useForm(props);
const blocks = useMemo(() => {
return [
{
id: "paragraph",
importFunc: async () => (await import("./ParagraphBlock)).default
}
]
}, []);
return (
<InlineForm form={form}>
<InlineGrid
row={RowBlock}
column={ColumnBlock}
blocks={blocks}
/>
</InlineForm>
);
}