A detailed and more in-depth explanation and how-to for a Webiny Headless CMS field plugins.
As an example, in this tutorial you will create a new field plugin that uses Google Maps API to retrieve a location or an address.
In this tutorial we assume that you know how Webiny plugins work.
You need a field that gives you a functionality to search for an address via Google Maps API.
When you select the address you want, from the list the Google Maps API provided, it is set to the form data.
Also, you do not want to index that field, and you want to encrypt it before saving into the storage.
By all means, you can create plugins where ever you want - but try to stick to some structure.
You have api and apps directories, so put your plugins in those, according to a plugin type.
Our suggestion would be something like this:
Your field type is a name of the directory containing all the plugins for that type.
Remember, there are multiple types of plugins for a single field type in both UI and API side.
Files we are creating are:
addressFieldPlugin.ts - API field definition
addressFieldStoragePlugin.ts - API storage modifications
addressFieldIndexPlugin.ts - API indexing modifications
addressFieldPlugin.tsx - UI field definition and display when creating a model
addressFieldRendererPlugin.tsx - UI display of the field when creating or updating the entry
First we need to create a field definition plugin. The base for the plugin is:
[UI]/addressFieldPlugin.tsx
exportdefault():CmsEditorFieldTypePlugin=>({
type:"cms-editor-field-type",
name:"cms-editor-field-type-address",
field:{
type:"address",
label:"Address",
description:"Search for the address",
icon:<AddressIcon/>,
allowMultipleValues:false,
allowPredefinedValues:false,
multipleValuesLabel:"Use as list of addresses",
createField(){}
}
});
And in the createField() function we return the field data:
[UI]/addressFieldPlugin.tsx
createField(){
return{
type:"address",
validation:[],
renderer:{
name:""
}
};
}
Note that renderer name is left blank so code automatically determines which renderer to use. You can put the name of the renderer, but for our tutorial we leave it blank and use it later.
Now we can create the second UI plugin, a renderer for the field we just created.
A base for the renderer plugin is:
[UI]/addressFieldRendererPlugin.tsx
exportdefault():CmsEditorFieldRendererPlugin=>({
type:"cms-editor-field-renderer",
name:"cms-editor-field-renderer-address",
renderer:{
rendererName:"addressRenderer",
name:`Address search`,
description:`Search for the address.`,
canUse({ field }){},
render({ field, getBind }){}
}
});
Remember that we left the renderer.name property blank when we created the field? Now in the canUse() function you put the condition that determines if that particular renderer is for a given field:
[UI]/addressFieldRendererPlugin.tsx
canUse({ field }){
return field.type==="address";
}
In the render() function you put what is actually displayed when the field rendering is called. We can create a onSelect() function that actually triggers the field change - bind.onChange().
[UI]/addressFieldRendererPlugin.tsx
render({ field, getBind }){
constBind=getBind();
constonSelect=(bind,{coordinates,...address})=>{
bind.onChange({
address,// an object containing country, city, zipCode, street and streetNumber
The manage side of the API can be exactly the same as the read one, but in our case it is a bit different.
Since we save plain JSON value, this part is quite simple:
Now we have everything required to create and save the field. Next thing we need is to prevent the indexing of the field. By default, if a field is not searchable it is removed from the index. But for this tutorial we can create our own plugin. This is the base of that plugin:
In toIndex we must remove the field from values and put it into non-indexable rawValues object:
[API]/addressFieldIndexPlugin.ts
toIndex({field, toIndexEntry}){
const values = toIndexEntry.values;
const value = values[field.fieldId];
delete values[field.fieldId];
return{
values,
rawValues:{
...(toIndexEntry.rawValues||{}),
[field.fieldId]: value
}
};
}
And in fromIndex we must revert that action:
[API]/addressFieldIndexPlugin.ts
fromIndex({ field, entry }){
const rawValues = entry.rawValues||{};
const value = rawValues[field.fieldId];
delete rawValues[field.fieldId];
return{
values:{
...(entry.values||{}),
[field.fieldId]: value
},
rawValues
};
}
This plugin now does what we want - disables the field indexing. Of course, we could pack the field with jsonpack or compress it in the toIndex method to take less space. The choice is all yours, just remember to revert what ever action you do.
All we have left to write is the plugin for storage. As we said, we want to encrypt the data for the storage. You can what ever library you want, it is all up to you. For this tutorial we can use some custom encrypt and decrypt functions. The base of the plugin looks like this:
[API]/addressFieldStoragePlugin.ts
exportdefault():CmsModelFieldToStoragePlugin=>({
type:"cms-model-field-to-storage",
name:"cms-model-field-to-storage-address",
fieldType:"address",
asyncfromStorage({ field, value }){},
asynctoStorage({ value }){}
});
First, we encrypt the data. Our suggestion is to encrypt the data and return an object with encrypted value and method of encryption:
[API]/addressFieldStoragePlugin.ts
asynctoStorage({ value }){
return{
encryption:"webiny",
value:encrypt(value)
};
}
Of course if you want to just return the encrypted value, feel free, the choice is yours.
And then comes the decryption. Because we expect our value to have some structure, it is easy to check if decryption is necessary. Or if we need to throw an error due to something we expect is not there.
[API]/addressFieldStoragePlugin.ts
asyncfromStorage({ field, value }){
if(!value){
return value;
}elseif(typeof value !=="object"){
thrownewError("It seems that value received is not an object.");
}elseif(!value.encryption){
thrownewError("Missing type of the encryption in the value object.");
}elseif(value.encryption!=="webiny"){
thrownewError(`This plugin cannot transform something not encrypted with "webiny".`);
}
returndecrypt(value.value);
}
You should now have a functioning field, apart the AddressSearch component and encrypt/decrypt functions - it is up to you to created them.
Remember that you need to import the plugins.
Default import file location for API is api/code/headlessCMS/src/index.ts and for UI it is apps/admin/code/src/plugins/headlessCms.ts.
If you changed that, import in these locations.