Forms in Drupal & GraphQL

Joao Garin

Joao Garin / February 07, 2025

14 min read

Most websites use forms. Most websites need forms. In this article I will show how our team at jobiqo have been using forms in decoupled using the drupal graphql module and Next.Js. The concept would apply to any other framework as well.

The idea / motivation

We have a lot of repositories and most of them share a lot of the code. This means that we re-use a lot of code for a lot of different purposes (for good or bad). As we started decoupling (way back in 2018) we knew forms was going to be a challenge and something we needed to solve.

At this point you might be asking why that is.

One of the most important parts in our platform is custom fields. And the ability to have custom fields (and rules inside fields) in each of our projects in not just one but across multiple entities.

So if we are looking at 100 repositories, many of them with custom fields, custom logic etc we need a way to do this in a maintainable / sane way. We can’t just build forms after forms in all these repos, in no time we would be looking at dozens and dozens of these.. and maintaining this with a small team would be close to impossible.

But first let's look at an example of a custom field, let's say we have a Job content type and we have some fields for it like a title a description and some relevant information for the job like desired skills, location, etc.

Now let's assume that we are talking about a job for an airplane pilot, there might be specific requirements (even legal requirements) for this type of job. Things like the license type, how many flight hours the applicant has, Aircraft type and so on. This is where we need to add some custom fields to this job entity.

This is what it looks like in action :

Custom fields on job

We had the form API before, that had its own set of issues but for this particular problem it had served us pretty well. We wanted something like that, or better.

The API

When working with decoupled one of the things I like the most is the ability to think about the API first (especially when doing things like this where your target group are other developers). Once you nailed the API then start walking backwards from it to understand what you have to build to make that work.

I remember looking at React JSON schema form and thinking “this is really cool, and quite close to what we need”. But even then it looked too complex. We know the Drupal entity and from there we should be able to get the information about all the fields, which ones are required, their default values, placeholders and so on. So we should be able to have a query that gets us this.

So what could this look like for a developer to use in its simplest form?

Something that not only respects but takes inspiration from the elegancy of React and this is what I thought was a good API for a component like this :

<EntityForm name="node" bundle="article" />

I am using the default article example here, following the example above the bundle would be job. But this approach works for other entities like profile, group or contact.

This is a simple component to use, and if it generates a full blown form (we will talk about the submission part later), it would be pretty nice. Put this on any page or component, and you get a form that respects the fields, and settings of that Entity.

One of the nice things about this is that we can develop custom modules that alter fields, add new fields, and it all gets reflected without any need to further changes in the frontend. What this means is we can reconfigure forms from the database or configuration via Drupal.

Not only that but as we have admins that need the same forms, we avoid the duplication of adding all this both in the backend and the frontend.

Going back to our probably now over 100 hundred repositories, this saved thousands and thousand of lines of code for us and a lot of nightmares.

We don’t duplicate forms, ever. Even when we need custom fields (some sites have e.g. a email / password only on registration but some have 4/5/6 custom fields) we do this via a custom module.

This also has allowed us to evolve forms over time. We started using Formik eventually moved to React Hook Form and also switched some validation libraries along the way. All of this pretty smoothly with some team members using this API without even knowing that this transition had happened. This was only possible because we designed our own API and are able to change the internals because the API our developers interact with does not change. When you entangle implementation details with API (the way developers interact with your components and code) you loose refactoring ability.

But more on how this actually works:

Drupal Entity definition query

When we started working on this, we (credits to Rado who worked on this closely with me to design this approach) created (initially in a custom module then ported to the official graphql module) the EntityDefinition query. It's main purpose was to retrieve all necessary information from any Entity, this includes everything from fields, to each field’s form settings and anything that’s necessary to build a form.

This is what this query looks like :

query EntityDefinition(
  $name: String!
  $bundle: String
) {
  entityDefinition(
    entity_type: $name
    bundle: $bundle
  ) {
    label
    fields {
      id
      label
      required
      multiple
      maxNumItems
      ...
    }
  }
}

From here we knew technically this was now possible. We had all the information about how to render a form for this entity.

We now had to actually create the EntityForm component that displays all the fields that we need.

The entity form component

The entity form component unfortunately was never open sourced (the idea was thrown around a couple of times) but our implementation of it has its own quirks and custom things inside but its implementation is way simpler than what it seems.

It is a lot of code, but not a a lot of complicated code. In its core it’s a component with a big switch statement checking the field type (integer, dates, boolean, term, address, as well as cardinality and other things) and then returning a component such as an input, a select etc (or multiple).

(The code bellow is pseudo code)

switch (field.type) {
    case 'text_long':
      return (
        <TextLongWidget entityField={field} fieldRules={fieldRules} />
      );
    case 'string_long':
      return (
        <StringLongWidget
          entityField={field}
          fieldRules={fieldRules}
        />
      );
    case 'string':
      return (
        <StringWidget entityField={field} fieldRules={fieldRules} />
      );
    case 'datetime':
      return (
        <DateSingeWidget
          entityField={field}
        />
      );
    default:
      return null;
  }
}

So essentially this component has a few jobs :

  1. Run a query to get information about the entity
  2. Check the fields returned by the entity
  3. for each field render the appropriate widget (component)
  4. Do some data massaging for preparing data for submission
  5. Handle validation
  6. Handle submission

Supported widgets

We have plenty of fields by now that we support using this solution :

  • Text
  • Text Long
  • Integer
  • Date (single and multiple)
  • Boolean
  • Taxonomy (multiple and single references)
  • Address (can be a normal address field or use react-geosuggest based on some configuration on the form settings)
  • Email
  • Phone
  • Term level (based on the Term Level module)
  • File (single and multiple)
  • Image (single and multiple and also a crop similar to what some drupal modules do for allowing the users to crop an image)

So a pretty extensive list of fields, at some point also no matter what entities you make there is only so much to make up in terms of form fields. Users are used to a certain type of UI and keeping things within that range makes a lot of sense.

Field Rules

In some situations fields only make sense based on previous data / selection. This is usually done in Drupal’s Form API using the #states property of form elements.

We have a similar system baked into this EntityForm component where we configure fieldRules in a similar way and we can pass those when needed to the EntityForm.

We use them for hiding, disabling, making fields required or even hiding parts of the taxonomy tree when certain rules (values of other fields) are met

field_salutation: {
  hidden: {
    title: 'Salutation hide'
  }
},

This will either make the salutation field hidden or not based on the value of the title field.

Edit Resume

We use Entity forms for the whole Resume editing which is a page with multiple sections, and uses the Profile module, like this :

<EntityForm name="profile" bundle="resume" />

Mutations

So we can make forms but what about submissions?

Submissions are handled in GraphQL with Mutations and mutations can also be somewhat automated, and even though we never really fully automated this part we got it to a pretty good point.

We use GraphQL and this whole article is based on that (even thought I would think this would be possible with JSON API as well)

But one of the important aspects with GraphQL that’s relevant for this case is that we don’t use the field names from Drupal (on purpose that's kind of the point).

That’s both a blessing and a curse here though. Because of this we need some mapping of the field names in the GraphQL API and the drupal machine names.

What we do for this is we have a config file which defines what fields we want (e.g. on some entities there are fields we don't want to show in the frontend for example). This config file is a javascript file that is used by the component that's rendering the form, and that config file defines not only what fields we want to use (which can be different per site), the field rules (if any), but also provide the mapping for the fields (the api name and drupal name).

Here is what e.g. a section of the resume shown above can look like in terms of configuration :

job_preferences: {
  key: 'job_preferences',
  name: 'Enter your Job preferences',
  description: `Provide the recruiters more information about your job preferences.`,
  fields: {
    field_resume_title: 'title',
    field_resume_job_pref_occupation: 'desiredOccupation',
    field_resume_employment_type: 'employmentType',
    field_resume_pref_location: 'desiredLocation',
    ...
  },
  fieldRules: {
    ...
  },
},

The important part here is the fields object which is a mapping of api field name to drupal machine name and also the fieldRules as I described above.

What this accomplishes is that we can generate a form based on the configuration we have provided and also provide the submission data in the right format (what the API expects).

<EntityForm
  name="profile"
  bundle="resume"
  onSubmit={(formData) => {
    // formData is already in the format Drupal (GraphQL) expects.
    callMutation({
      variables: {
        input: formData
      }
    });
  }}
  ...
/>

(there are a lot more options we can pass to the component, I am trying to put here only the important parts)

This makes mutations simple enough, and still leaves room for some "custom" things to be done within the callback (AKA it's not "too" automatic).

⚠️ A note on the structure of fields in GraphQL, Inputs and Types

In order for this to work we do need to make sure we stick to some level of consistency on fields across all the different types and entities we use. A taxonomy field should use the Term type (GraphQL type) and a TermInput (Input type). This was never an issue because we never intended to do it differently, but its a good thing to keep in mind.

If we have a term field with tid and label (a taxonomy reference) we need to make sure that all terms follow this pattern (which is a good idea anyway) so we make a Term type in GraphQL and use that (same for the Input).. and so whenever we have a term we know the structure of it.

We also use GraphQL codegen which means we have access to the GraphQL schema in typescript so we would know if something changes that would break this.

Closing remarks

I hope this made some sense, and that it was at least not too hard to follow. There are of course even more nuances and complexities that I have somewhat hidden away here in order to make this a bit more comprehensible.

There's a lot when it comes to forms, and it's a bit overwhelming at times but this particular "project" was one of the most interesting and challenging ones (of the many) in the last few years. It has served our company very very well (in my somewhat biased opinion) and the flexibility we are able to provide to clients is very on par with what we have always had (which is far more than most platforms do as we know) but with the ability of improving our UI and UX greatly (also on forms).

Why did we never open source this component?

Open sourcing the EntityForm component is not very easy. There are a couple of reasons for it, but it would definitely be possible and I would argue also a great addition to projects like Next Drupal, Drupal Decoupled, next-drupal-starterkit, DrupalX or any of the other projects trying to make decoupling Drupal easier.

Some of the challenges are :

  • HTML: Our component is simple to call, but it embeds all the HTML (React widgets for each field type using our own Design system) and in a open source package you don't want to force people into styling approaches or enforce too much the structure of the HTML.
  • GraphQL : We only use GraphQL and so there are some things within the component that assume that (including querying for the Entity information)
  • Form libraries : Even worse than forcing users into a specific style library would be to force them in a specific form library. Some of these are big, and some of them are just not very well maintained (that's e.g. why we moved away from Formik)

Given all of this, I still think there is room for handling some of the heavy lifting and giving users a hook or something that would make it easy for people to create their own forms for any entity they need.

Lets explore some ideas of how this could look like. For JSON API the field mapping is probably not even necessary :

Component API (similar to the one above)

<EntityForm
  name="node"
  bundle="article"
  mapping={someFieldMapping}
  onSubmit={(formData) => handleSubmit(formData)}
  widgets={{
    // This could be optional and the library would still render some default version of each widget.
    text_long: CustomTextLongWidget
  }}
/>

The main idea here would be to give the possibility to customize the widget for each field type. We would still have the actual form library inside (or just use React 19 form api)

Hook API

const { fields } = useDrupalEntityForm({
  name: 'node',
  bundle: 'article',
  mapping: someFieldMapping
});

return (
  <form onSubmit={submitHandler}>
    {fields.map((field) => (
      <FieldRenderer key={field.id} field={field} />
    ))}
    <button type="submit">Submit</button>
  </form>
);

Here we already give the user more control over both the styling and the actual form generation. We would need a good definition of what a "field" is and what are the options available for it. But as a consequence it does "less".

It can still probably do the querying, handle the data massaging and so on.

I hope this was a fun read, and thank you for sticking to it until the end ;)

I would like to thank everyone that contributed to our form solution over the years, everyone in the product team at jobiqo and for everyone that helped review this article.

Subscribe to the newsletter

Get emails from me about web development, tech, and early access to new articles.