Extensions: What They Are and How to Build Them

Chris Corbett
February 28, 2020
5 mins
EngineeringGuides

With Dynamic Content you already have the ability to define any data structure you need and then use the automatically generated forms to create content. This concept is at the core of what makes DC powerful and with Extensions we're taking it to the next level.

For certain use cases using form fields isn’t the most effective method of editing your content. If you are doing something visual, perhaps you want to crop a source image to your liking, entering x & y percentages in a text box isn’t the best experience.

Image editing using dc-extension-di-transform

Sometimes you want your editing controls to more closely represent what is rendered in the destination app. There may also be more efficient entry methods for the particular data structure you want to use.

One example of this is a tree data structure. You may want to represent a menu structure in your app and while it’s possible to create a Schema that will match this structure, using the form fields to generate it would be cumbersome.

Using dc-extension-tree to generate a mega-menu

Finally, if you have data in another service that you would like to use, it can be difficult to bring this into your content. Finding product SKUs for example would require you to bring up your product inventory, search and then copy / paste the SKU into the form.

Using dc-extension-product-selector to fetch product information

With Extensions you can replace a field control in the content form with a control of your liking. This brings with it more expressive and powerful methods of creating your content.

While we already have a growing list of our own extensions, you can also create your own.

Building an Extension

Creating a connection

An Extension is essentially a mini web app that runs in an iFrame which sits in place of the native form control. The web app needs to communicate with the parent app in order to send and receive information, this is all handled for you in the SDK.

1import { init } from 'dc-extensions-sdk';
2const sdk = await init();

Here we are importing the init function and using it to establish the connection. You should notice this is an asynchronous process. Behind the scenes the app is using PostMessage to communicate to the parent app that it should initiate a MessageChannel connection. The parent app then uses this connection to send contextual information to the SDK in order to create a new sdk instance.

Best practice tip #1: Prioritise the loading and initiation of the SDK over other scripts and resources in your app. If the connection takes too long to establish, the content form will fall back to the native form control.

Retrieving and setting values

Once we have established a connection the next thing we want to do is retrieve the value associated with the field the Extension is in control of.

1const fieldValue = await sdk.field.getValue();
2console.log(fieldValue);

You’ll notice that this method also returns a promise. Once it resolves it will contain the current value of the field. If the Content Item is new, or no value has been saved, this value will be `undefined`.

You may have already guessed what you need to do in order to set a value:

1const errors = await sdk.field.setValue('some value');

The difference is if the value you sent doesn’t pass validation against the defined JSON Schema (e.g. if you provide a string and it is expecting a number) the promise will reject and provide an array of errors. These will tell you exactly what was wrong with the value you supplied. This will also trigger visible validation errors in the content form.

Best practice tip #2: If you use TypeScript (and why wouldn’t you) you can define the shape of the field data to benefit from strong typing:

1const sdk = await init<FieldModel>();

You can choose to rely on this validation messaging to inform the user, or you can test the validation of the new value using 

1sdk.field.isValid('some value');
 or 
1sdk.field.validate('some value');
 before you attempt to set the value to handle the errors inside the extension.

Gotchas

So far we’ve learned the basics of using the SDK. We can establish a connection, we can get and set a value of a field. Now here are a few in-the-weeds gotchas you might want to be aware of.

Sandbox

The iFrame has the following sandbox values set on it: 

1allow-scripts allow-popups allow-popups-to-escape-sandbox
. This means certain Web APIs like 
1window.alert();
 and 
1window.prompt();
 will not be useable.

It is also worth noting that any XHR request sent from the sandboxed app will have an origin of 

1null
. As a result, any APIs or services you intend to use will need appropriate CORS settings.

Context is important

The content editing form is usable in a number of different parts of the DC app, not all of them will have access to all the features of the content form. For example you cannot use choosers in the Content Type Schema Editor, as such 

1sdk.mediaLink.getImage();
 will reject in this context.

In some circumstances the content editing form is a ‘read only’ mode. You will want to handle this in your extension so that editing controls are disabled.

Best practice tip #3: Use the readOnly property of the form object to determine if the Extension can be editable:

1if (sdk.form.readOnly) {
2  disableExtension();
3}
4sdk.form.onReadOnlyChange(readOnly => {
5  if (readOnly) {
6    disableExtension();
7  } else {
8    enableExtension(); 
9  }
10});

Using an Extension

Once you have your extension built there are two main ways to associate an extension with a field in your Content Type Schema. Both require adding a new ui:extension property.

One way is to point it to the URL where your extension is located:

1{
2  "properties": {
3    "myCustomProperty": {
4      "type": "string",
5      "ui:extension": {
6        "url": "https://url-to-extension.app"
7      }
8    }
9  }
10}

The other way is to register a new extension and reference it by name.

1{
2  "properties": {
3    "myCustomProperty": {
4      "type": "string",
5      "ui:extension": {
6        "name": "my-registered-extension"
7      }
8    }
9  }
10}

Additional properties

There are two additional properties that can be added to the extension object:

height

This height takes a number value which will set the height of the component in the form before it has loaded. Although the form will automatically resize to the height of your control once it has loaded, it is useful to set this in order to prevent jarring pop-ins.

params

These parameters allow you to set configuration options that can be picked up by your component. They are made accessible via the SDK by using `sdk.params.instance`.

Best practice tip #4: If you are intending to put sensitive information in your params (such as API keys) you should register the extension and use installation params instead. The URL you use for your extension should also be trusted.

Using them all together:

1{
2  "ui:extension": {
3    "name": "my-registered-extension",
4    "height": 300,
5    "params": {
6      "any": "options",
7      "can": "be put here"
8    }
9  }
10}

Best practice tip #5: You can define the shape of the props to benefit from strong typing in TypeScript:

1const sdk = await init<FieldModel, Props>();

Further Reading

That’s all you need to get started creating extensions. We can’t wait to see what you’ll make!