Sorry, no results found for "".

Plugin SDK > Working with form values

Working with form values

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.

Localized fields

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');

Modular Content fields

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.

Avoid creating Editor field extensions for Modular Content fields!

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.

Field extensions on block fields

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);

Structured Text fields

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.

Avoid creating Editor field extensions for Structured Text fields!

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.