DEV Community

Maciej Krawczyk
Maciej Krawczyk

Posted on • Originally published at Medium on

Keystone.js custom fields: text with autocomplete

keystone
Photo by Bicanski on Pixnio

Problem background

Some websites and apps I’m working for my clients are research projects in humanities especially in archeology. And because of that their needs are pretty specific. On the one hand they require simple webpage to present their research results and fulfil administrative obligations, but on the other hand, they also require custom tailored tools to catalog research data. Usually these are qualitative rather than quantitative data.

Typically, my stack to fulfil this needs is pretty extensive. It starts with WordPress as CMS, working in headless mode with Next.js frontend. And this covers website part. Data collection tool was in most cases custom-made app in Node.js and Express or PHP app with Laravel framework. It was completed by Elasticsearch and another large tools. But most of the time I have gut feeling that’s a bit of overcomplicated solution and for sure bloated one. When I was starting current project, I made decision that I have to find better and lighter way to do that, especially because in this case data model was not so complicated. My research started with looking for other CMS, best in Node.js and headless out of the box. I knew about two of this kind of systems, Strapi and Keystone.js. First one is a little to rigid for my taste, but second one looks like perfect solution for my needs. Possibility to define schemas in code and easy configuration was really nice. But after that, I had great aha moment. What if I can also use it to define models for data catalog? Is there possibility to kill two birds with one stone? I believe there is. There was one issue thou, but about this later.

About Keystone.js

On Keystone website we can read that it is superpowered CMS for developers, with built-in GraphQL support and Management UI in place. Also, there’s new, just released version 6. It has really impressive set of features, authentication and authorization, UI, it uses TypeScript and much more useful and necessary features. The only thing we have to worry is our entities structure. But there are a couple of problems. Abstraction layer is really thick in here. In general, it’s not an issue but in some places may become a concern.

Built-in set of fields which can be used in lists and schemas here is more than enough in most basic cases. But unfortunately my case turned out to be not so basic. Maybe it’s caused by specificity of my project, but I needed some extra fields to work with. The issue was not in the field types provided by Keystone, text or JSON fields are versatile enough to serve many purposes. But their visualization in admin panel can be insufficient sometimes.

During development of this project I had to create three custom fields, but only their UI aspects had to change. Underneath they still are text and JSON fields. First of this three is text field with additional autocomplete and I gonna focus on that in this article.

Requirements

Text field with autocomplete? It sounds like relationship field from Keystone basic set. Yes, but not really. Relationship means in that case that there are at least two entities connected to each other. In this case I just need to get suggestions from table column corresponding to that field. So, what this field has to have? First, it has to have a possibility to get all suggestions from database (or search engine in my case) while typing, then present them to user and let her/him select one or finish writing own version.

Documentation info about custom field

Mostly because it’s new, just released version of Keystone.js documentation is not so clear in some places. But part about using custom fields is really nice. Even though for me the most useful part of it was the link to source code of built-in fields (for the moment when I am writing this the link is broken, use this instead).

In our case interesting is text field (code). We can use this file as a template for what we want to achieve. In documentation, it’s stated that custom field view have to exports four things: field controller and three React components — Field, Cell and CardValue. Each one responsible for different visualizations. But here we are in comfortable position, to fulfill our requirements we have to change only Field visualization, others are fine, so we can reuse built-in ones. Also, there are no changes in controller, so original one can be reused too.

Steps to resolve

First what I did was to create separate folder views to hold all custom fields visualizations and subfolder autocomplete for our current visualization. Main file there is view.tsx containing our new field. Because we won’t be modifying field controller and Cell and CardValue components, so we can import them and re-export right away.

I believe it’s not necessary, but in sake of completion and code readability is reasonable to do it. Also, here we can add rest of necessary imports:

Here we are importing basic React hooks which will be necessary later, some types from Keystone core library, and built-in field components to blend in our custom components into other fields in Admin UI. Now is time to create our Field component end exports it. Also, we have to declare some basic state of the component. We are going to need it later.

According to documentation props in this case have several properties: field which is object containing all methods and properties form field controller, value holding field value, in this case as an object:

Mostly it depends on kind of operation we are currently performing. It looks different when user creates entity and differently when it’s updated. Also, there are two boolean values autoFocus and forceValidation, both rather self-explanatory. Last one there is method onChange which is value setter here. Additionally, I am not pretty sure why, but it is optional, and we have to keep that in mind. Ok, now it’s time to construct our component JSX:

There are to main parts of this component. First one creates text input using built-in components (with some extra events handlers) and simple suggestions list created using ul and li tags. Also, here we can see usage of visible component state. It’s responsible for showing list of suggestions on focus event (and hiding on blur).

Skeleton of the component is in place, now we have to add some muscles to it and add missing methods onChange (our one, not the one coming from props), onSuggestionClick and setOnChange helper.

First of these methods, setOnChange is a kind of helper function responsible for calling props.onChange method and setting new value of field. As I mentioned before, it’s optional, so we have to close it inside if statement and then call it with new value (keeping in mind, how this value should look like). Next one, onChange fires on each change event. It calls SetOnChange first and then starts to look for suggestions, if the input is more than 3 characters long it calls async method getSuggestions with current input value and then calls set state method setSuggestions. Otherwise, if input is too short it sets suggestions to empty array. I believe, in some cases it maybe necessary to debounce this method, but I’ve decided not to. Last one onSuggestionClick sets field value to the value of clicked suggestion.

Next we need to create previously mentioned method getSuggestions:

Its purpose is simple, it fetches data from server in JSON format and returns suggestions. In this case it is a custom endpoint created according to docs (more):

It’s simple Express.js route calling autocomplete method and returning results as JSON. It takes three parameters: query to search, index to search in and field to return. In my case under the hood it uses Meilisearch search engine and its JS integration — Meilisearch JavaScript. It’s really nice, reliable and what’s the most important lightweight search engine, useful alternative to huge Elasticsearch. But I am using it because it’s also needed in other part of the system, in other cases using built-in QueryAPI should be enough.

Lastly we have to add a bit of reactiveness to our component, in order to do that we have to add two useEffect hooks and one useCallback:

First, we create memoized callback to load initial suggestions on component loads and value is already set (in case of editing record). I used this hook to make sure that this method is only declared once, on component render. And then we call it inside first useEffect hook, the one running only on component render. Whole idea here is to already have the suggestions when user starts editing the corresponding field. Last hook changes visibility of suggestions based on length of suggestions state array.

Now our component is working as planed, but there’s still one issue. It’s not pretty at all, so we have to add some styling to blend it better with the rest of UI. I moved entire styles to separate file just to not make it messy, but it’s strictly personal.

Here I’ve decided to use Emotion library just because I know it enough, and it does its job perfectly. Styles here mostly mimic built-in ones to help our custom component to blend into UI and look like it was always there.

So, here’s complete component:

Component in action
Component in action

Summary

To sum up: using custom fields in Keystone.js is not that hard (at least custom visualizations for fields). It requires a bit of research and clear idea what we want to achieve, but it can easily resolve minor problems. And most important can save use from writing full-fledged custom system where it’s not needed. When I was starting my first job as a junior web developer, someone told me that real dev knows when to use library/framework and only adopt it and when is better to make custom solution. I believe even though couple of years passed I am still learning this.

To be honest it’s the first article I’ve ever written. I hope it’s clear and informative, and I really appreciate your feedback. I am also planing to write about another two custom fields I’ve needed to create. And I am challenging myself to publish one each week. I hope it will work out. See you in the next one!

Top comments (0)