This article is the second one in the series about Keystone.js custom fields. Here you can find the first one.
Projects I am usually working one are rather small but has its own specificity due to their background (more about that in first article). In most cases they contain full-fledged CMS system and custom-built data collection tool. But last one left me wondering is there any way to kill two birds with one stone and simplify whole system. Solution here was to use Keystone.js possibility to create models for content data but also for research qualitative data. The only challenge here was that built-in set of fields is nice but in some cases too basic. Here we are going to focus on map field allowing user to store localization of points in database (coordinates to be exact). Like in the previous one, built-in text field was sufficient to store data, but I had to create visual part of this input almost from scratch.
Main goal here was to create field showing interactive map to the user allowing to zoom and pan view and also click to add point. Then as a result save coordinates of this point into database. Also, we have to store this data into text field. Alternatively it can be stored in two separate columns in database, one for latitude and one for longitude. But I believe it’s more complicated solution, it requires custom field controller and also changing backend part of the field (see details). In that case solution with one text field seams like much better. To sum up, we need to:
- Display map,
- Add controls (pan and zoom),
- Add possibility to add point to map,
- Save point coordinates to database.
Fortunately we don’t have to build everything from scratch. Most of the heavy lifting will be handled by OpenLayers library. There are many NPM packages handling maps, but the most important advantage of this one is great and complete documentation (most parts). So first we have to add it to our Keystone project:
$ yarn add ol
I am using Yarn here, but also you can install it using NPM:
$ npm i ol
Additionally, due to some dependencies mismatch I had to install separately geotiff.js, depending on actual version at the moment you read this it may not be necessary.
$ yarn add geotiff
Like in previous component I’ve created separate subfolder coordinates for this field in views folder. Basic component structure is the same as in previous component, so we have to import controller, Cell and CardValue from built-in version of text component and reexport them. Also, I’ve setup basic JSX using built-in FieldContainer and FieldLabel components.
The base of our map component here is this div tag. And basically that’s all JSX needed. Whole logic and map rendering is going to happen inside this useEffect hook. Additionally, I had to add useRef hook to keep reference to that mentioned before div.
First, we need to import needed elements from ol library:
Basically map created with OpenLayers is only a container, we have to add layers in order to present our desired map. First, I created base map layer source using TileImage class and map tiles from Digital Atlas of the Roman Empire (more info):
Then, I had to create map instance:
Here as you can see Map requires a couple of configuration properties. First, we have to set reference to DOM element which will contain our map, mapRef.current in that case. Next property is an array of initially created layers. Here I’ve created TileLayer based of source created before. Last property here is view, it sets map initial center (coordinates, here in EPSG:3857 coordinate system) and zoom. Actually this is the only one obligatory property when creating map (docs). After this steps, we have ready map visualization which can be controlled by user. Next, we have to add another layer to hold point created by user. In this case it’s VectorLayer with corresponding VectorSource and set of styles for points. And then we have to add it into our existing map.
Additionally, here I’ve created styling for the point added by the user. In order to do that I have to instantiate Style object with configuration with property image. There are other ways of doing it, but I prefer this one (check docs). Value of this property is instance of Circle class (in my case aliased as CircleStyles), with configuration object containing radius, fill and stroke props. Also last two are instances of corresponding classes. Basically it sets point visualization to circle with radius of 5 pixels, red, slightly transparent fill and opaque red border. Now map is ready to add our custom handler for singleclick event to allow user to add point. But first we need a way to store our point coordinates:
Also, here in case of situation when field already have value (e.g., when we are editing the record) we are setting coordinates variables to this value. This little complicated way of reading value is mostly caused by the way that Keystone internally handles data for text field. Ok, next we have to create handler for the event I’ve mentioned before:
To create this handler we have to call on method on our map object. It takes two parameters, string with event type and callback function which has one parameter, evt being type of MapBrowserEvent. Basically there are two purposes of this callback, to set new value of field (also lat and lon variables) and call addPointAndFocus method. Here it is:
This method does three things, if variables lat and lon exists and has values then it removes all previous points. In our case there can be only one to remove, but method getFeatures returns array. Important thing to note here is that we are operating on vectorSource not vectorLayer. Next, new point feature is created with current coordinates. Lastly map view is set to have center on newly created point and increased zoom (in case if it’s smaller than 8). And now our component is almost ready, lastly we have to add a bit of styles to it. Mostly because map container has to have height set to value in pixels:
One last thing to add was all styles from OpenLayers library. Here I’ve used React-Helmet package to help with that task:
Ok, here is finished component, and how it looks like inside the app:
Creating new fields in Keystone maybe easier than it looks, I hope I was able to show that. At first It may look daunting, but it’s no different from creating other ordinary React components. Everything depends on our requirements, and how complicated they are. Also, libraries like OpenLayers may be a little scary at first glance, additionally quick start tutorial in documentation is focused mainly on usage in static sites (by static I mean static like ten or more years ago) what can cause some problems to users used to current approach with single page applications and gigabytes of NPM packages. But when we dig a little deeper API documentation is really great and informative.
This was second article in series about custom Keystone field components and I planed to finish it with the next one about slightly more complicated menu component, utilizing JSON field underneath. But when I was writing this one I realized that this topic is pretty niche and there’s a need for simpler, more introductory overview of Keystone.js as a CMS. So, the next one will be kind of Keystone.js v 6 101, and then we will get back to menu custom field. See you in the next one!
Top comments (0)