reactjskeystonejs

Display two schema fields as concatenated label in KeystoneJS


I am setting up my schemas and AdminUI in KeystoneJS (v6). I work with names and want to display the combined first and last name in the Select field in the frontend:

So instead of having 'Tommy', 'Markus' and so on, I want 'Tommy Smith', 'Markus Brown'.

Current frontend

This is the code I have:

import { list } from '@keystone-next/keystone/schema';
import { text } from '@keystone-next/fields';

export const Participant = list({
  ui: {
    labelField: 'firstName',
  },
  // fields
  fields: {
    firstName: text({ isRequired: true }),
    lastName: text({ isRequired: true }),
    }),
  },
});

Does anyone have any pointers how I could do this?

Thank you!


Solution

  • When selecting related items in Keystone, the Admin UI uses item labels ā€“ a human readable identifier assigned to each item. The value used for the item label is controlled using the ui.labelField list config:

    ui.labelField: Selects the field which will be used as the label column in the Admin UI. By default looks for a field called 'label', then falls back to 'name', then 'title', and finally 'id', which is guaranteed to exist.

    ā€“ Keystone 6: Schema API docs

    In your example, you've configured Keystone to use the firstName field as the label, which works ok, but presents the problem you've identified. What we really need is 3rd field that combines both the first and last names together.

    Now, we could do this with hooks, by combining the values and saving them to another text field for example. But this would duplicate the data and we'd need to remember to keep the values in sync, adding complexity. Better to just concatenate these values when they're being read out of the DB. For this, we can use virtual fields.

    In the example given, the virtual field solution looks like this:

    import { list, graphql } from '@keystone-next/keystone';
    import { text, relationship, virtual } from '@keystone-next/keystone/fields';
    
    export const Participant = list({
      ui: {
        // Configure the list to use the fullName field for it's labels
        labelField: 'fullName',
      },
      fields: {
        firstName: text({ validation: { isRequired: true } }),
        lastName: text({ validation: { isRequired: true } }),
        // Add a fullName field that concatenates the participants first and last names together
        fullName: virtual({
          field: graphql.field({
            type: graphql.String,
            resolve: (item) => `${item.firstName} ${item.lastName}`,
          }),
        }),
      },
    });
    

    Easy, right? Then, in the Admin UI, you'll get something like this:

    enter image description here

    By default, the label is also used in the list view so you'll notice the extra info there are well:

    enter image description here

    If you don't want this, you can additionally set the ui.listView.initialColumns list config (docs here).

    There's a pretty good guide on virtual fields that's worth a look. It covers more advanced usage, like loading fields from other items, returning complex types, etc.