Cover
TinaCMS

Let's create a conditional field for TinaCMS

Introduction
Let's create a conditional field for TinaCMS

I am currently on fire with TinaCMS, I am really excited to get it working with Baretheme. I was missing an option to conditionally show or hide fields in TinaCMS so I just went ahead and created it myself. Here's how you can do it with Gatsby!

If you don't know TinaCMS

Check out my other article:

TinaCMS: probably the best way to edit your Gatsby site in 2020
A colleague recently pointed me on TinaCMS [https://tinacms.org/] and I was interested immediately. I was looking for a solution to develop my own theme with and I already decided GatsbyJS [/exploring-tinacms/gatsbyjs.org/plugins/] would be the way to go. TinaCMS seemed like a perfect match. Build…

What we will build

We will build a component that's called condition which takes a trigger and fields. This time however, fields will be a function that receives the value of the field. Based on this you get a powerful tool to show or hide fields. The trigger can be anything, be it a toggle or a select. Also you can add as many fields to the condition as you need. Here is an example using the toggle:

{
          label: 'Internal',
          name: 'internal',
          description: 'Set to false if you want link to another website',
          component: "condition",
          trigger: {
            component: "toggle"
          },
          fields: (value) => {
            return value ? [
              {
                label: "Link",
                name: "link",
                component: "document",
                parse(value) {
                  return value || ""
                },
              }
            ] : [
              {
                label: "Link",
                name: "link",
                component: "text",
              }
            ]
          }
        },

And this would be the equivalent using a select:

{
          label: 'Internal',
          name: 'internal',
          description: 'Set to false if you want link to another website',
          component: "condition",
          trigger: {
            component: "select",
            options: ["Yes", "No"],
          },
          fields: (value) => {
            return value === "Yes" ? [
              {
                label: "Link",
                name: "link",
                component: "document",
                parse(value) {
                  return value || ""
                },
              }
            ] : [
              {
                label: "Link",
                name: "link",
                component: "text",
              }
            ]
          }
        },

I know people love animated GIFs, so here is one:

Register the field

In gatsby-browser.js add the following:

import ConditionField from "./src/fields/condition"

export const onClientEntry = () => {
  window.tinacms.fields.add({
    name: "condition",
    Component: ConditionField,
  })
}

And create the field at the specified location:

import React from 'react';

const Condition = ({ input, field, form }) => {
 return <div>Condition</div>
};

That's straightforward! You can already use the field if you add component: "condition" to one of your fields. You will just see the text "Condition". For now.

Let's create the trigger

So first we need some way to trigger our condition. For this you specify a trigger attribute on your field. The trigger will contain just the information that you need for a field, it can be anything. What you won't need here is the name and label, because we will mix this together with our original condition field.

const fields = [
    {
      ...field.trigger,
      name: field.name,
      label: field.label,
    },
  ]

This will create a new array called fields, take everything we get from field.trigger and overwrite the name and label props. That's it already!

But wait, we only got the data right. Now we need to build the components.. so do we actually have to rebuild all the components on our own? No! I found a neat helper when I studied the core of TinaCMS: it exposes a FieldsBuilder from @tinacms/form-builder. How nice! Let's use it.

const Condition = ({ input, field, form }) => {

  const fields = [
    {
      ...field.trigger,
      name: field.name,
      label: field.label,
    },
  ]

  return (
	<FieldsBuilder fields={fields} form={form} />
  )
};

This will already render the trigger! Unfortunately the FieldsBuilder contains padding that we cannot simply change, also it doesn't allow you to add more props to it's DOM element. So we have to create a wrapper. For this we use styled-components via import styled from 'styled-components'. And change our return statement to:

return (
    <ConditionalField>
      <FieldsBuilder fields={fields} form={form} />
    </ConditionalField>
  )

And we finally create the field:

const ConditionalField = styled.div`
  margin-top: -20px;
  margin-left: -20px;
  margin-right: -20px;
`

It's a little hacky but for now it works, until they provide a better version to customize it (or I find a better way).

Creating nested fields

Now we need something our trigger can trigger. In this case it should trigger the specific fields definition. Let's first make our fields function work. Add this:

const nestedFields = field.fields(input.value);

This is super simple for us and you now get super powers in your fields definition. You can use any value not just booleans and render different fields. Brilliant? Yes.

Next we need to mix our nested fields together with our main field as we don't want to add another level to our data. Do it like so:

const conditionalFields = nestedFields.map(f => {
    const fieldPath = field.name.split('.').slice(0, -1)
    const name = fieldPath.concat(f.name).join('.')
    return {
      ...f,
      name,
    }
  })

We iterate through our nested fields. Our main field field has a name that's more like a path in JSON, it has this shape: foo.rawJson.0.bar.baz. The baz in this example is the actual name of our field. Our nested fields need to get the same path with their own name. So we split this string at each dot and convert it to an array. Then we remove the last part of the array via slice(0, -1). We get the name of our nested field and put the array together to a string with join('.'). Then we simply return the nested field and overwrite the name.

All we need to do know is mixing it together with our main field:

const fields = [
    {
      ...field.trigger,
      name: field.name,
      label: field.label,
    },
    ...conditionalFields
  ]

That's it!

You can go ahead and copy and paste the code from my Gist over here:

TinaCMS condition field
TinaCMS condition field. GitHub Gist: instantly share code, notes, and snippets.

I will probably go ahead and create a package containing this.. or maybe it will find it's way into the core of TinaCMS? Let's see.

Also checkout how to create a relation field:

Let’s create a relation field for TinaCMS
I am currently migrating Baretheme [https://baretheme.com] and while modeling my content I’ve come to a point where I was in need for relation fields. I try to use those fields as much as possible in order to avoid losing connection between data. In this guide I will use TinaCMS together with Gatsb…
Marc Mintel
Author

Marc Mintel

Marc Mintel is a self taught JavaScript and Frontend Developer with heavy focus on React and Vue.

View Comments
Next Post

Introducing: tinacms-fields

Previous Post

Let's create a relation field for TinaCMS

Success! Your membership now is active.