react-tinacms-inline

This package provides the components and helpers for Inline Editing.

Install

yarn add react-tinacms-inline

For specific steps on setting up Inline Editing on a page, refer to the Inline Editing documentation.

Inline Form

The InlineForm component provides an inline editing context. To use, wrap the component where you want to render inline fields and pass the form.

Usage

export function Page(props) {
  const [, form] = useForm(props.data)
  usePlugin(form)

  return (
    <InlineForm form={form}>
      <main>
        {
            //...
        }
      </main>
    </InlineForm>
  )
}

Interface

interface InlineFormProps {
  form: Form
  children: React.ReactElement | React.ReactElement[] | InlineFormRenderChild
}

interface InlineFormRenderChild {
  (props: InlineFormRenderChildOptions):
    | React.ReactElement
    | React.ReactElement[]
}

type InlineFormRenderChildOptions = InlineFormState &
  Omit<FormRenderProps<any>, 'form'>

/**
 * This state is available via context
*/
interface InlineFormState {
  form: Form
  focussedField: string
  setFocussedField(field: string): void
}
KeyDescription
formA Tina Form.
childrenChild components to render — Either React Elements or Render Props Children that receive the form state and other Form Render Props

useInlineForm

useInlineForm(): InlineFormState

The useInlineForm hook can be used to access the inline editing context. It must be used within a child component of InlineForm.

Inline Field

Inline Fields should provide basic inputs for editing data on the page and account for both enabled / disabled CMS states. All Inline Fields must be children of an InlineForm.

Interface

export interface InlineFieldProps {
  name: string
  children(fieldProps: InlineFieldRenderProps): React.ReactElement
}

export interface InlineFieldRenderProps<V = any>
  extends FieldRenderProps<V>,
    InlineFormState {}
KeyDescription
nameThe path to the editable data.
childrenChild components to render — React Elements that receive the form state and Field Render Props.

Available Inline Fields

See the full list of inline fields or learn how to make custom inline fields.

Below is a list of fields provided by the react-tinacms-inline package:

Inline Field Styles

Styles are stripped as much as possible to prevent interference with base site styling. When toggling between enabled / disabled states, the default inline fields will switch between rendering child elements with any additional editing UI and just passing the child elements alone.

For example with InlineText:

export function InlineText({
  name,
  className,
  focusRing = true,
}: InlineTextProps) {
  const cms: CMS = useCMS()

  return (
    <InlineField name={name}>
      {({ input }) => {
        /**
        * If the cms is enabled, render the input
        * with the focus ring
        */
        if (cms.enabled) {
          if (!focusRing) {
            return <Input type="text" {...input} className={className} />
          }

          return (
            <FocusRing name={name} options={focusRing}>
              <Input type="text" {...input} className={className} />
            </FocusRing>
          )
        }
        /**
        * Otherwise, pass the input value
        */
        return <>{input.value}</>
      }}
    </InlineField>
  )
}

Input is a styled-component with some base styling aimed at making this component mesh well with the surrounding site. If you ever need to override default Inline Field styles, read about this workaround to extend styles.

Focus Ring

The common UI element on all Inline Fields is the FocusRing. The focus ring provides context to which field is active / available to edit.

Interface

interface FocusRingProps {
  name?: string
  children: React.ReactNode | ((active: boolean) => React.ReactNode)
  options?: boolean | FocusRingOptions
}

interface FocusRingOptions {
  offset?: number | { x: number; y: number }
  borderRadius?: number
  nestedFocus?: boolean
}

Focus Ring Props

You would only use these options if you were creating custom inline fields and working with the FocusRing directly.

KeyDescription
childrenOptional: Child elements to render.
nameOptional: This value is used to set the focused / active field.
optionsOptional: The FocusRingOptions outlined below.

Focus Ring Children

FocusRing optionally accepts render prop patterned children, which receive the active state and can be used to conditionally render elements based on whether the FocusRing currently has focus.

<FocusRing>
 {active => {
   if (active) {
     return <ComplicatedEditableComponent />
   }
   return <SimpleDisplayComponent />
 }}
</FocusRing>

Focus Ring Options

These options are passed to default inline fields or inline block fields via the focusRing prop on most default inline fields. The options are configurable by the developer setting up the inline form & fields. Refer to individual inline field documentation for additional examples.

KeyDescription
offsetOptional: Sets the distance from the focus ring to the edge of the child element(s). It can either be a number (in pixels) for both x & y offsets, or individual x & y offset values passed in an object.
borderRadiusOptional: Determines (in pixels) the rounded corners of the focus ring.
nestedFocusOptional: Disables pointer-events (clicking) and hides blocks empty state.

Inline Blocks

Inline Blocks consist of an array of Blocks to render in an Inline Form. Refer to the Inline Blocks Documentation or Guide for code examples on creating Inline Blocks.

Interface

export interface InlineBlocksProps {
  name: string
  blocks: {
    [key: string]: Block
  }
  className?: string
  direction?: 'vertical' | 'horizontal'
  itemProps?: {
    [key: string]: any
  }
  min?: number
  max?: number
  components?: {
    Container?: React.FunctionComponent<BlocksContainerProps>
  }
  children?: React.ReactNode | null
}
KeyPurpose
nameThe path to the source data for the blocks.
blocksAn object composed of individual Blocks.
classNameOptional — To set styles directly on the input or extend via styled components.
directionOptional — Sets the orientation of the AddBlock button position.
itemPropsOptional — An object that passes additional props to every block child element.
minOptional — Controls the minimum number of blocks. Once reached, blocks won't be able to be removed. (Optional)
maxOptional — Controls the maximum number of blocks allowed. Once reached, blocks won't be able to be added. (Optional)

Actions

The Inline Blocks Actions are used by the Inline Blocks Controls. Use these if you are building your own custom Inline Block Field Controls. These actions are avaiable in the Inline Blocks Context.

interface InlineBlocksActions {
  count: number
  insert(index: number, data: any): void
  move(from: number, to: number): void
  remove(index: number): void
  blocks: {
    [key: string]: Block
  }
  activeBlock: number | null
  setActiveBlock: any
  direction: 'vertical' | 'horizontal'
  min?: number
  max?: number
}

useInlineBlocks

useInlineBlocks(): InlineBlocksActions

useInlineBlocks is a hook that can be used to access the Inline Blocks Context when creating custom controls.

Context and inline blocks children

To conditionally render markup adjacent to your inline blocks, depending on context values, pass children to the InlineBlocks and call useInlineBlocks in your child component(s). For example, this will render some text below your inline blocks, but only if there are more than three inline blocks currently on the page:

import { InlineBlocks, useInlineBlocks } from 'react-tinacms-inline'

export function LotsOfBlocksMessage() {
  const blocks = useInlineBlocks()
  return <>
    {blocks.count > 3 &&
      <p>
        Wow, there are {blocks.count} blocks, that's rather a lot!
      </p>
    }
  </>
}

export function MyBlocksContainer() {
  return (
    <InlineBlocks name="myBlocks" blocks={MY_BLOCKS}>
      <LotsOfBlocksMessage />
    </InlineBlocks>
  )
}

Inline Block Field Controls

Editors can add / delete / rearrange blocks with the blocks controls. They can also access additional fields in a Settings Modal.

Interface

interface BlocksControlsProps {
  index: number
  insetControls?: boolean
  focusRing?: false | FocusRingProps
  customActions?: BlocksControlActionItem[]
  label?: boolean
  children: React.ReactChild
}

interface FocusRingProps {
  offset?: number | { x: number; y: number }
  borderRadius?: number
}

export interface BlocksControlActionItem {
  icon: React.ReactNode
  onClick: () => void
}
KeyDescription
indexThe index of the block associated with these controls.
insetControlsA boolean to denote whether the group controls display within or outside the group.
focusRingEither an object to style the focus ring or false, which hides the focus ring entirely. For styles, offset (in pixels) controls the distance from the ring to the edge of the group; borderRadius(in pixels) controls the rounding edge of the focus ring.
labelA boolean to control whether or not a block label is rendered.
childrenAny child components, typically inline field(s).
customActionsAn array of objects containing custom block action configuration. icon is the component to render in the toolbar. onClick handles the action behavior.

Block Definition

A block is made of two parts: a component that renders in edit mode, and a template to configure fields, defaults and other required data.

Interface

interface Block {
  Component: React.FC<BlockComponentProps>
  template: BlockTemplate
}

interface BlockComponentProps {
  name: string
  index: number
  data: any
}

interface BlockTemplate {
  label: string
  defaultItem?: object | (() => object)
  fields?: Field[]
}

Block Component

KeyPurpose
nameA unique identifier and pseudo-path to the block from the parent blocks array. e.g. the first child would be 'blocks.0'
indexPosition in the block array.
dataThe source data.

Block Template

KeyPurpose
labelA human readable label.
defaultItemOptional — Populates new blocks with default data.
fieldsOptional — Populates fields in the Settings Modal.

Examples

Block Components can render Inline Fields to expose the data for editing. When using Inline Fields within a block, the name property should be relative to the block object in the source data.

For example:

import { useForm } from 'tinacms'
import { InlineForm, InlineBlocks, BlocksControls, InlineTextarea } from 'react-tinacms-inline'

function FeaturePage({ data }) {
  const [ , form ] = useForm({
    id: 'my-features-id',
    label: 'Edit Features',
    fields: [],
    initialValues: data,
  })

  return (
    <InlineForm form={form}>
      <div className="wrapper">
        <InlineBlocks name="features_blocks" blocks={FEATURE_BLOCKS} />
      </div>
    </InlineForm>
  )
}

function Feature({ index }) {
  return (
    <BlocksControls index={index}>
      <div className="feature">
        <h3>
        {/**
        * The `name` property is relative to individual
        * `features_blocks` array items (blocks). The full path
        * in the source file (example below) would be
        *  `features_blocks[index].heading`
        */}
          <InlineTextarea name="heading" focusRing={false} />
        </h3>
        <p>
          <InlineTextarea name="supporting_copy" focusRing={false} />
        </p>
      </div>
    </BlocksControls>
  )
}

const featureBlock = {
  Component: Feature,
  template: {
    label: 'Feature',
    defaultItem: {
      _template: 'feature',
      heading: 'Marie Skłodowska Curie',
      supporting_copy:
        'Muse about vastness.',
    },
    fields: [],
  },
}

const FEATURE_BLOCKS = {
    featureBlock
}

Example JSON data

{
    "features_blocks": [
        {
            "_template": "feature",
            "heading": "Drake Equation",
            "supporting_copy": "Light years gathered by gravity Rig Veda.."
        },
        {
            "_template": "feature",
            "heading": "Jean-François Champollion",
            "supporting_copy": "Not a sunrise but a galaxyrise."
        },
        {
            "_template": "feature",
            "heading": "Sea of Tranquility",
            "supporting_copy": "Bits of moving fluff take root and flourish."
        }
    ]
}

Below is another example using InlineBlocks with multiple block definitions:

import { useJsonForm } from 'next-tinacms-json'
import { InlineForm, InlineBlocks, BlocksControls, InlineTextarea } from 'react-tinacms-inline'

export default function PageBlocks({ jsonFile }) {
  const [, form] = useJsonForm(jsonFile)

  return (
    <InlineForm form={form}>
      <InlineBlocks name="my_blocks" blocks={PAGE_BLOCKS} />
    </InlineForm>
  )
}

/** Example Heading Block Definition
 * Component + template
*/
function Heading({ index }) {
  return (
    <BlocksControls index={index}>
      <InlineTextarea name="text" />
    </BlocksControls>
  )
}

const heading_template = {
  label: 'Heading',
  defaultItem: {
    text: 'At vero eos et accusamus',
  },
  fields: [],
}

const headingBlock = {
    Component: Heading,
    template: heading_template
}

/**
 * Example Paragraph Block
 * Component + template
*/

function Paragraph({ index }) {
  return (
    <BlocksControls index={index} focusRing={{ offset: 0 }} insetControls>
      <div className="paragraph__background">
        <div className="wrapper wrapper--narrow">
          <p className="paragraph__text">
            <InlineTextarea name="text" focusRing={false} />
          </p>
        </div>
      </div>
    </BlocksControls>
  );
}

const paragraphBlock = {
  Component: Paragraph,
  // template defined inline
  template: {
    label: 'Paragraph',
    defaultItem: {
      text:
        'Take root and flourish quis nostrum exercitationem ullam',
    },
    fields: [],
  },
};

/**
 * Available blocks passed to InlineBlocks to render
*/

const PAGE_BLOCKS = {
    headingBlock,
    paragraphBlock
}

Configuring the Container

InlineBlocks wraps your blocks with a <div> element by default. This can be an issue if your styles require direct inheritance, such as a flexbox grid:

<div class="row">
  <div class="column">
  </div>
</div>

To handle this, you can pass a custom container component to InlineBlocks.

Interface

interface BlocksContainerProps {s
  className?: string
  children?: React.ReactNode
}

Example

import { useJsonForm } from 'next-tinacms-json'
import { InlineForm, InlineBlocks, BlocksControls, InlineTextarea } from 'react-tinacms-inline'

const MyBlocksContainer = (children}) => (
  <div>
    {children}
  </div>
)

export default function PageBlocks({ jsonFile }) {
  const [, form] = useJsonForm(jsonFile)

  return (
    <InlineForm form={form}>
      <InlineBlocks
        name="my_blocks"
        blocks={PAGE_BLOCKS}
        components={{
          Container: MyBlocksContainer
        }}
      />
    </InlineForm>
  )
}

Checkout this guide to learn more on using Inline Blocks.


useFieldRef: Ref-Based Inline Editing

useFieldRef is the first part of an experimental new API for creating an inline editing experience with Tina. With useFieldRef, inline editing components are defined in the form configuration, and you assign a ref to the component in your layout that the field should attach to.

Inline fields created in this fashion will be absolutely positioned on top of the referenced component and conform to its dimensions. This makes it possible for the field to appear as if it were replacing the layout component in the DOM without altering the markup.

Setting up ref-based inline editing

Adding inline editing with this API is done in three steps:

1. Add the RBIEPlugin

This feature is experimental for now, so in order to enable it, you need to add it as a plugin to your CMS config.

import { TinaCMS } from 'tinacms'
import { RBIEPlugin } from 'react-tinacms-inline'

const cms = new TinaCMS({
  //...
  plugins: [
    new RBIEPlugin()
  ]
})

2. Add an inlineComponent to your form field definition

Similar to how you would configure a sidebar field via that field's component key, you can configure an inline field via the inlineComponent key.

Unlike the sidebar components, inlineComponents must be defined as a React component and not a string. In the future, inlineComponent will accept a string as well and components can be registered via the plugin system.

import { useForm } from 'tinacms'

function MyPageComponent() {
  const [data, form] = useForm({
    //...
    fields: [
      //...
      {
        name: 'title',
        component: 'text',
        inlineComponent: ({ name }) => (
          <h1><InlineText name={name} /></h1>
        )
      }
    ]
  })
  //...
}

3. Call useFieldRef and attach the ref to the component that you want to be editable

useFieldRef requires the ID of the form that it corresponds to, as well as the name of the field that it should be editing.

import { useForm } from 'tinacms'
import { useFieldRef } from 'react-tinacms-inline'

function MyPageComponent() {
  const [data, form] = useForm({
    //...
  })
  const titleRef = useFieldRef(form.id, 'title')
  return (
    <InlineForm form={form}>
      <main>
        <h1 ref={titleRef}>{data.title}</h1>
      </main>
    </InlineForm>
  )
}

View on GitHub ->