Inside of Sidebar panels and Field extensions you have access to ctx.formValues
, which contains the complete internal form state for the record that the current user is editing. With that, you can access its work-in-progress changes, and react to them.
The structure of ctx.formValues
is heavily dependent on the fields of its model. In fact, the keys of this object are the model's field IDs:
{"title": "Foo bar","cover_image": {"upload_id": "32943530""alt": null,"title": null,"focal_point": null,"custom_data": {},},"author": "39832254","seo": {"image": "16229550","title": "Hugo","description": "With Hugo, you can build amazing static projects","twitter_card": "summary"},}
If you want to change the value of some field, you can use the ctx.setFieldValue
method:
await ctx.setFieldValue('title', 'new value');
Most of the field values you'll find are 100% identical to their respective Content Management API formats (see the section "Field type values"), even tough there are a couple of important exceptions we'll cover below.
If a field is localized, the format of ctx.formValues
will slightly change, similarly to what happens on the Content Management API (see the section "Localized fields"):
{"title": {"en": "Foo bar","it": "Antani"}}
In this case, to change the field value in English, you need to pass the complete field path to ctx.setFieldValue
:
await ctx.setFieldValue('title.en', 'new value');
As you know, modular content fields contain blocks, which are complex structures composed of multiple inner fields. If you inspect the value of a modular content field from ctx.formValues
, you'll see something like this:
[{"itemId": "39830695","itemTypeId": "810886","social": "twitter","url": "https://twitter.com/datocms",},{"itemId": "39830696","itemTypeId": "810886","social": "linkedin","url": "https://www.linkedin.com/company/35537033"}]
Every block contains the itemId
(ID of the block) and itemTypeId
(ID of the block model) attributes, while all the other attributes depend on the actual fields of the block model.
You can edit the value of a Modular Content field just like any other field. Following the example above, you could ie. reorder the existing blocks by social using the ctx.setFieldValue
method:
const currentValue = ctx.formValues['my_modular_content'];await ctx.setFieldValue('my_modular_content',currentValue.sort((a, b) => a.social.localeCompare(b.social),);
But you can also remove some blocks:
await ctx.setFieldValue('my_modular_content',currentValue.filter(block => block.social !== 'linkedin'),);
Or even add new blocks to the field:
await ctx.setFieldValue('my_modular_content',[...currentValue,{"itemTypeId": "810886","social": "twitter","url": "https://twitter.com/datocms",},],);
Pay attention to the missing itemId
attribute here: when the record will be eventually saved, a new itemId
will be generated by the DatoCMS API.
While it's perfectly fine — and as we just saw, quite straightforward — to develop Addon field extensions for Modular Content fields, overriding the regular editor DatoCMS offers for this field type is generally not a good idea, as you'll need to handle the rendering and update of all the fields and blocks it contains. Not an easy task.
If a Field Extension is installed on a field belonging to a block, nothing really changes. You can get the value of the specific field of the block using ctx.fieldPath
:
import get from 'lodash-es/get';// ctx.fieldPath for a block field will be something// like "my_modular_content.1.title"get(ctx.formValues, ctx.fieldPath);
If you inspect the value of a Structured Text field from ctx.formValues
, you'll see something like this:
{"my_structured_text_field": [{"type": "paragraph","children": [{"text": "Meet "},{"text": "the best way","highlight": true},{"text": " to manage content with Hugo"}]}]}
Even with this tiny one-paragraph example, you'll notice that this format is quite different from the dast
format that both CMA and CDA offers:
There's no root
node: the value is directly an array of root children;
Nodes of type span
have no type
attribute, the value
attribute is called text
, and marks
are applied as boolean keys directly on the node itself.
To offer a comparison, this would be the dast
version of the same content:
{"my_structured_text_field": {"schema": "dast","document": {"type": "root","children": [{"type": "paragraph","children": [{"type": "span","value": "Meet "},{"type": "span","marks": ["highlight"],"value": "the best way"},{"type": "span","value": " to manage content with Hugo"}]}]}}}
Why is that? Because to power Structured Text fields, under the hood, the DatoCMS application uses the (awesome) Slate Editor library. Its internal representation format is somewhat different from dast
, and continuously converting back-and-forth from the two formats on every key stroke was infeasible from a performance point of view.
So, how can you overcome this constraint?
If your plugin just needs to read Structured Text fields, without ever changing their value, you can use the slateToDast
function exposed by the datocms-structured-text-slate-utils
package to convert the internal Slate format into regular dast
, and then do your reading on its result:
import { slateToDast } from 'datocms-structured-text-slate-utils';import groupBy from 'lodash-es/groupBy';const allFieldsByItemTypeId = groupBy(Object.values(ctx.fields), field => field.relationships.item_type.data.id);const dast = slateToDast(ctx.formValues['my_structured_text_field'],allFieldsByItemTypeId,);// result will be something like://// {// schema: 'dast',// document: { type: 'root', children: [...] },// }
If you want to read AND write the content of a Structured Text field, then the datocms-structured-text-slate-utils
package offers complete Typescript types and type guards for the Slate format, so you know what you can expect to read and write in there.
In this example, we're building a function that removes every link present in the content:
import { Node, isLink, isNonTextNode, NonTextNode } from 'datocms-structured-text-slate-utils';import clone from 'clone-deep';function visit(tree: Node | Node[],callback: (node: Node, index: number, parents: Node[]) => void,) {const all = (nodes: Node[], parents: Node[]) =>nodes.forEach((node, index) => one(node, index, parents));const one = (node: Node, index: number, parents: Node[]) => {if ('children' in node) {all(node.children, [node, ...parents]);}callback(node, index, parents);};if (Array.isArray(tree)) {all(tree, []);} else {one(tree, 0, []);}}function removeLinks(slateValue: Node[]) {const value = clone(slateValue);visit(value, (node, index, parents) => {if (!isNonTextNode(node) || !isLink(node)) {return;}const parent = parents[0] as NonTextNode;parent.children.splice(index, 1, ...node.children);});return value;}ctx.setFieldValue('my_structured_text_field',removeLinks(ctx.formValues['my_structured_text_field'] as Node[]),);
Structured text fields can contain both references to other records via its itemLink
and inlineItem
nodes, and blocks via its block
nodes. The Slate representation for them is similar to the following:
[{"type": "paragraph","children": [{"text": "This is a "},{"type": "itemLink","item": "78722383","itemTypeId": "810907","children": [{"text": "link to a record"}]},{"text": " and this is an inline record: "},{"type": "inlineItem","item": "69045807","itemTypeId": "810907","children": [{ "text": "" }]}]},{"type": "paragraph","children": [{"text": "This is a block:"}]},{"type": "block","id": "87031498","blockModelId": "810933","children": [{ "text": "" }],"title": "Foobar"}]
As you can see:
Both itemLink
and inlineItem
nodes have item
and itemTypeId
attributes that point to the referenced record;
Both inlineItem
and block
nodes need to have a children
attribute always containing an empty span;
Blocks have the blockModelId
attribute containing to the ID of the block model and the id
attribute with the ID of the block, while all the other attributes depend on the actual fields of the block model itself.
You can create/remove/change these nodes like any other one by keeping their formats correct. In this example, we're adding a new block node at the end of the content:
ctx.setFieldValue('my_structured_text_field',[...ctx.formValues['my_structured_text_field'],{type: 'block',key: `${new Date().getTime()}`,blockModelId: '810933',title: 'Foobar',children: [{ text: '' }],},]);
Pay attention to the key
attribute here: to create new block nodes, you need to fill it with an unique string. When the record will be eventually saved, a new ID will be generated by the DatoCMS API, the id
attribute will appear in the node, and the key
attribute will be removed.
While it's perfectly fine to develop Addon field extensions for Structured Text fields, overriding the regular editor DatoCMS offers for this field type is generally not a good idea, as it requires a lot of effort to re-create a convincing editing experience.