Control Canvas Height

Is there any way to style the Canvas/iframe element for a plugin?

It seems to have a fixed height of 20px and I’m not always needing to have the Canvas take up space. I’m essentially showing available actions based on the state of multiple input fields and when I don’t have any available actions, I don’t want the plugin to take up space.

Ideally, I’d like to see some kind of way to set the height of the Canvas, which would apply to the iframe, but I don’t see any available properties to do so and I assume all styling available to a plugin is contained within the iframe.

Follow-up on this issue:

  • No Canvas: One of our plugins returns an empty element, rather than canvas and the iframe ends up being 55px tall
  • Empty Canvas: min side is 20px tall

Bumping this issue again. This is a critical issue for us.

It is causing us to build plugins in odd ways like implementing field extension functionality in a form outlet to avoid the fixed space penalty for each field extension, regardless whether something needs to be displayed to the user.

@nroth

In the component, you can use ctx.stopAutoResizer() with ctx.updateHeight(0) to set the iframe height to 0. Then when you need to show it again, you can use ctx.startAutoResizer() to resume the resizing.

Demo video

Source code

interface AddonProps {
    ctx: RenderFieldExtensionCtx;
}

const ShowHideIframe: React.FC<AddonProps> = ({ctx}) => {
    const isFieldOn = useMemo(() => ctx.formValues['boolean_field'], [ctx.formValues])

    useEffect(() => {
        if(isFieldOn) {
            ctx.startAutoResizer()
        } else {
            ctx.stopAutoResizer()
            ctx.updateHeight(0)
        }
    }, [isFieldOn, ctx])

    return (
        <Canvas ctx={ctx}>
            <div style={{background: 'red', color: 'white', padding: '5px'}}>
                    Boolean field is currently: `{isFieldOn ? 'ON' : 'OFF'}`
            </div>
        </Canvas>
    );
};

For both this issue and the other plugin one, I invited you to a sample project if you want to see how they work:
https://forum-nroth-plugins.admin.datocms.com/editor/item_types/UGBkPE3TR8O3NlqWwV2CPg/items/GJJ53DV8TYeyPt8FZ4ONfQ/edit

(They both use the arcataroger/datocms-plugin-dropdown-overlay source repo)

This is very helpful, going to play around with this. I saw these methods, but thought that the initialHeight: 0 property on the field extension would essentially be providing that initial height for me. I think this should work for us when leveraging field add ons.

Controlling Entire Field Visibility by Field Extension Editor

One follow-on question that I think is related to managing the visibility of things outside the iframe: I was also working on a dynamic select field that would only show once it is populated with values.

This is essentially like the conditional select type of plugin, except the dropdown selection is driven off of data from an api, rather than being fixed values. (we are using this as a stop-gap as we transition off our legacy CMS). I built it as a field extension editor using the json field where you have essentially the selection, then options to select from.

Setup

  • Form has two fields, 1 being the primary field, then the second being the dropdown field that depends on external data from our legacy cms
  • The idea would be that the second field would be completely hidden on the form until the user selects a value in the primary field
  • We have a separate plugin that is a general purpose plugin for fetching external data based on field value state. So, when it sees field 1 change, we fetch external data and set field 2.

Workflow

  • User loads the form, field 1 is visible and i want field 2 to be hidden completely
  • User selects a record for field 1
  • Our general-purpose data fetching plugin fetches data.
  • It sets the value of the second field (dynamic select) with data that kind of looks like {"selection": [], "options": [{"label": ..., "value": ...}, {"label": ..., "value": ...}, ...]}.
  • At this point, I want to show field 2 now that the user can actually interact with it

The issues I had was:

  • If I don’t render the select field when it is empty, I have the same issue with the height. However, I also still am seeing the field title. I want it to not show at all
  • The toggleField method seems to completely disable the field extension. I’m able to toggle the field off within the field extension when the value of the field is empty, but then the field extension seems to never get triggered again to rerender when I set the value of the field
  • I just tried using the stopAutoResizer and updateHeight method for this plugin and while it does eliminate the height of the field, it doesn’t get rid of the field title

Code Example

This should hopefully highlight what I’m trying to do. I’m just wondering if it intended for toggleField to completely disable a field, rather than just hide/show it (as described in the docs).

I’m sure you might say, why not fetch the data in the field extension, but the idea here was to create a reusable field type for whenever we want to do something like this. Then, there is just the broader need to fully hide a field based on conditional behavior that it seems like plugins have trouble doing.

import { RenderFieldExtensionCtx } from "datocms-plugin-sdk";
import { Canvas, SelectField } from "datocms-react-ui";
import get from "lodash/get";
import { useEffect } from "react";

type PropTypes = {
  ctx: RenderFieldExtensionCtx;
};

type DynamicSelectOption = {
  value: string;
  label: string;
};

type DynamicSelect = {
  selection: DynamicSelectOption[];
  options: DynamicSelectOption[];
};

export function DynamicSelectField({ ctx }: PropTypes) {
  // The value of the field
  const currentValue = get(ctx.formValues, ctx.fieldPath)
    ? JSON.parse(get(ctx.formValues, ctx.fieldPath) as string)
    : ({} as DynamicSelect);

  // reference to the current selected option(s), which starts out empty
  const selection: DynamicSelectOption[] = currentValue
    ? currentValue.selection
    : [];

  // all options for the user to chose from
  const options =
    currentValue && currentValue?.options
      ? currentValue.options.map((option: any) => {
          return { value: option.value, label: option.label };
        })
      : [];

  // Update the json value of the field when the user makes a selection
  const handleChange = (newValue: any) => {
    ctx.setFieldValue(
      ctx.fieldPath,
      JSON.stringify({ selection: newValue, options: options })
    );
  };

  // Manage the visibility of the field editor based on currentValue.options
  useEffect(() => {
    if (options.length === 0) {
      ctx.toggleField(ctx.fieldPath, false); // If I toggle it off, it never will get updated once options.length > 0
      ctx.stopAutoResizer();
      ctx.updateHeight(0);
    } else {
      ctx.toggleField(ctx.fieldPath, true); // I never reach this point
      ctx.startAutoResizer();
    }
  }, [options, ctx.fieldPath, ctx.formValues]);

  return (
    <Canvas ctx={ctx}>
      <SelectField
        id={ctx.field.id}
        name={ctx.field.attributes.api_key}
        label={""}
        value={selection}
        selectInputProps={{
          isMulti: false,
          options: options,
        }}
        onChange={handleChange}
      />
    </Canvas>
  );
}

Hey @nroth,

I think you’re right that toggleField() will remove the field from the page altogether, making it so that the second field can’t re-show itself. I’ll check with the devs to see if this is intended behavior.

In the meantime, I think a simple workaround is to just put that logic in a Form outlet instead, which lives invisibly at the top of the record (but doesn’t necessarily have to render any UI, and has its own lifecycle independent of any fields):

You can see the demo here: Loading...

In index.tsx, I added:

     itemFormOutlets(model, ctx) {
        switch (model.attributes.api_key) {
            case 'dynamic_select':
                return [
                    {
                        id: 'DynamicFieldHider',
                        initialHeight: 0,
                    }]

            default:
                return [];
        }
    },

    renderItemFormOutlet(
        outletId,
        ctx,
    ) {
        render(<DynamicFieldHider ctx={ctx}/>);
    },

And moved the hide/show logic into a new entrypoints/DynamicFieldHider.tsx:

import {RenderItemFormOutletCtx} from "datocms-plugin-sdk";
import {useEffect} from "react";

type PropTypes = {
    ctx: RenderItemFormOutletCtx;
};

export function DynamicFieldHider({ctx}: PropTypes) {

    // Manage field visibilities
    useEffect(() => {
        switch (ctx.formValues['field1']) {
            case 'hide':
                console.log("Hiding Field 2")
                ctx.toggleField('field2', false)
                break;
            case 'show':
                console.log("Showing Field 2")
                ctx.toggleField('field2', true)
                break;
        }
    }, [ctx.formValues]);

    // We don't need to show any UI for this
    return null;
}

Updated the demo project with that example. It wasn’t super clear to me how your async fetches worked, so I just simulated a simple timeout in the “Dynamic Select” field editor plugin for field2 (DynamicSelectField.tsx). But how that works doesn’t really matter since the hide/show logic is moved to the Form Outlet instead.

Does that help?

PS I invited you to that sample repo so you can edit & PR to it too. Can we please use that (or feel free to invite me to yours instead, if you prefer) so we can look at real examples?

With situations like this where there are different plugins interacting with each other, it might be easier to have an actual example project to work with instead of trying to decipher blocks of text :slight_smile:

Demo repo here: GitHub - arcataroger/datocms-plugin-dropdown-overlay

Demo project here: Loading...

@nroth, the devs have confirmed that the current toggleField() behavior is intentional (i.e., it is supposed to remove the field altogether). We have updated the docs to be clearer about that.

That means the FormOutlet workaround above (Control Canvas Height - #7 by roger) is probably the way to do this.

Will that work for you?

I’m still exploring options and it could be a workaround, but I think it would be worth putting in a feature request for a hideField() method that does actually hide a field, but keep it available on the form to use by plugins.

The example you shared makes sense, but I think would require some odd behavior to work. For example, you are triggering off the field name of dynamic_select in the form outlet, so i’d have to name my fields with some identifier like that. I don’t think there is a way that I could tell to lookup in the form outlet initialization to find if a model has any fields that are using a particular plugin.

You can see that the Computed Field plugin that we are using has the same problem. This is partially why we are building some of our own plugins.

I think that would be nice too, but it might take a while =/ Feature Requests tend to get worked on based on their votes…

Sorry if I wasn’t clear, but the FormOutlets happen at the record level, not a specific field. The dynamic_select is just the name of the model. All records of that type will have this form outlet, regardless of their fields.

In my example, the actual fields are called field1 and field2. The plugin is attached to them via a manual field extension, but you can also use the regular, non-manual extensions to programmatically override them: Field extensions — DatoCMS

I’m sorry if my example was a bit contrived; it wasn’t super clear to me how your fields tied together, so I tried my best to make a simple example. If you can point me at the project you’re working on, I can try to modify the plugin to make it work with the real thing?

Inside a FormOutlet, you should be able to use ctx.loadFieldsUsingPlugin() to get the fields using the current plugin, or ctx.fields to get all fields, and then parse their attributes.appearance.field_extension. e.g.:

ctx.fields:

[
    {
        "id": "L5U7uRghSEOgUHY3H9_HHA",
        "type": "field",
        "attributes": {
            "label": "Field2",
            "field_type": "json",
            "api_key": "field2",
            "hint": null,
            "localized": false,
            "validators": {},
            "position": 2,
            "appearance": {
                "addons": [],
                "editor": "PXCWoGLJTM25LDopBghV1g",
                "parameters": {},
                "field_extension": "DynamicSelectField"
            },
            "default_value": null,
            "deep_filtering_enabled": false
        },
        "relationships": {
            "item_type": {
                "data": {
                    "id": "dRrrkWdSSL-_dpUwlwxESA",
                    "type": "item_type"
                }
            },
            "fieldset": {
                "data": null
            }
        }
    },
   // etc
]

Can I help you change the FormOutlet to work the way you need it to?

Sorry, which problem specifically? You mean this general thread, or is there some issue with a specific call?

Gotcha, sorry, missed that reference to model.

Ok, yeah i think that would work.

Sorry, I did play around with this particular extension more, but I am still not sure if I’m still going to use this approach since each field takes up so much room in the editor, but the hiding of fields and controlling the height is definitely relevant to the larger plugin I am working on. I think you shared enough that I could definitely wrap it up.

The other approach I was considering was instead of having these dynamic selection fields as part of the model, I was considering showing the same kind of options as a field extension/editor. I have been testing that approach again given that you shared the example for Dropdown Button Overlay. I got that working and was able to tweak your suggested approach to support the case where a user clicks on the dropdown, but doesn’t click anything. It just still feels a tad janky.

Our core challenge with Dato (besides being trapped in an iframe) is that we have a fair number of computed and dependent fields we use. The plugin system allow us to do what we need to, but since they only operate when a user is editing the content within the dato form, we are having to work through architecturally how we want to manage this functionality when we need it to be applied both in the UI when editing in real time and if we were to publish content through the API.

1 Like

Gotcha. Yeah, at a certain level of complexity, the field editors/extensions can become difficult to work with, especially if you’re editing data elsewhere too.

Have you considered, for certain models, just building your own UI around the CMA API instead and just presenting it as a custom page inside Dato? Custom pages — DatoCMS That’s basically an entire iframe inside Dato that you control, with some helper context that we pass to it if you need it.

You end up having to recreate all the fields, but if you’re not using anything too complex (like structured text), but rather just a bunch of drop-downs, that might be easier than trying to wrestle the plugin system? You’d have full control over that page’s internal state, validations, dependencies, etc., and then just manually save the data to Dato after your internal computations are complete.

Yeah, we have, but generally that is a big piece of why we are making the move from a custom-built CMS was to avoid having to do that.

Hey nroth, I feel your pain here, and I’m truly sorry I don’t have a better answer for you :frowning:

It’s probably becoming clear to you now, but Dato largely tries to cater to the mid-complexity use cases where human editors are doing most of the edits, and other data sources just provide supplementary information on occasion (if ever).

At a certain level of complexity, this deliberate focus means we may not be the best solution for a “single source of truth”/data lake type use case, especially for fast-changing dynamic or real-time data that requires humans-in-the-loop. Really we’re more of a nice GUI editor on top of some databases and basic APIs, not necessarily a replacement for something full-blown like Salesforce (or even Redis or Shopify, or some extensible open-source CMS). Realistically, this means we’re not (and can’t be) the best tool for every job…

In a bigger company, situations like this (which is quite different from most of our customers’ use cases) might become a custom dev project, scoped out and co-developed by the vendor and client together. But to my knowledge, we don’t offer anything like that (even for enterprise), and probably wouldn’t have the staff to do that anyway (with only 6-7 devs on the team, depending on how you count them).

Anyway, that doesn’t really help you here… =/ What I can offer isn’t much, but:

  1. I’ll at least speak to the devs about Feature Requests overall and whether we can make them more “responsive”/transparent somehow. Right now it feels like posts there can languish for a while, even for things that seem straightforward. If that happens, maybe the Plugins architecture could be incrementally improved in a more visible way…? If there are specific requests/bugfixes within that system that you’d like to see, please do continue to let us know. (We already have someone looking at the dropdown not expanding canvas properly issue, for example). Or if you have a proposal for a non-iframe-based/less-sandboxed plugin system, I’d be happy to bring it up with the devs.

  2. Beyond that… for what it’s worth, I was formerly a customer of Dato too before I started working here. Like you, my former org wanted to move off a bespoke CMS (in our case, an old and heavily tweaked Drupal). We prototyped some stuff in Dato and found that it could meet about 80% of our needs, but we also had some external data we wanted to sync in that Dato just didn’t support, or didn’t have good data types or editors for.

    We did end up having to do some custom development for that 20%, but that was overall still a lighter lift than having to maintain a completely homebrewed system. Obviously this is a very YMMV situation, but maybe a hybrid system doesn’t have to be completely ruled out…? Especially today, toolkits like MUI should hopefully make it relatively simple to whip up some relatively simple text boxes and dropdowns, etc. It might just be a faster situation for you than trying to elevate/shoehorn the relatively limited plugin system to new heights.

    To be fair, this approach (of combining custom UIs with regular Dato) mainly worked for us because our 20% of things weren’t really a core part of our editors’ daily usage, just small additional things. If they were core to your project’s intended functionality, it might be a different story :frowning:

    Like if you need to fetch a bunch of images and prices from all over, centralize them ASAP into Dato and have humans further tweak them… all in near-real-time… I totally understand the use case and the desire here, but it doesn’t sound like it would be easy at all. There’s a lot of async stuff going back and forth, on a limited plugin system that isn’t really designed for something so robust. I truly think that a custom UI would probably be the easier approach here, vs an uphill battle of trying to reshape the plugins into a much more powerful system (that most of our customers aren’t asking for right now).

    I know there are many other headless CMSes out there too, but I can’t think of one off the top of my head with a more powerful plugin system. (They might be out there, I’m just not super familiar with all of them). If there are any particular examples you’d like us to emulate instead, I’d be glad to share that with the devs too.


Anyway, sorry for the long response, and truly sorry I don’t have a better answer for you. As a smaller company, we really just don’t have the resources to expand our core feature set to meet every potential use case out there.

However, that said, we’ll still do our best to support any individual issues you may run into along the way. I hope you understand!

Custom Interface

Thanks, yeah I totally understand. I have actually started work on a prototype custom editor for our core content type, but plan for that to actually be a completely external tool. It essentially would be a custom chrome extension with some AI capabilities to pull data from the page you are currently looking at. So, I’m not against a custom interface, I’m just trying to find how we can get the most out of the standard forms within dato for the traditional use-case as a base-layer of functionality.

Native-Feeling Dato Plugins

The issues i’ve mostly been coming up against are where plugins have inherent limitations that make it impossible for them to feel like a native field and fully manage their visual presentation independent of their function. I think these aren’t needs that are necessarily specific to us.

I do think that Dato should strive to support plugins feeling as native as possible. There is a lot in there already like opening modals, opening item pickers, alerts, etc. However, if I use the dato react library to generate a custom select field, I’d expect that the select field would look and behave like a native select field.

It sounds like you have that feedback though and finding these limitations or quirks has helped me learn more about the framework, so I appreciate the discussion.

Our Specific Needs with Computed Fields

We did do an evaluation of other CMS options out there and there didn’t seem to be a lot of support of computed/templated fields, which is a big use case for us. Directus has some basic support that I think is using underlying postgres functionality for computed fields, but it wasn’t completely what we need. I found a surprising amount of capability to do this kind of thing in productivity tools (fibery.io which even has a graphql api, coda.io), but they aren’t well suited for production-level content production. So, Dato’s limitations around this wasn’t special.

I guess the area we didn’t quite fully anticipate is just how client-oriented dato’s plugin system is. We did discover this issue when trying out the Computed Fields plugin, so what I’ve built-out is a model in Dato that represents the transformations as input fields, some action to transform those inputs, then output fields. The idea being the plugin can use that data model to generate the transformed data client-side and a server-side component could use the same data model to say based on the state of this data, here are the transformations i need to execute if I’m pushing content into Dato via the API. I have this working, but was running into visual issues/artifacts that I wasn’t expecting (canvas height and the dropdown/select field issue).

1 Like

Hey @s.verna, I just wanted to highlight that post from @nroth for you too in case you have any thoughts: Control Canvas Height - #16 by nroth (Stefano is the one who architected the overall plugin system, I believe)

Stefano, you don’t have to read this entire novel of a thread, but in summary (nroth, please chime in if I missed anything):

  • Our plugin system has an accumulation of different individual bugs like the canvas height issue (which are reported and are already looking into, but individual bugs aren’t the main problem)
  • Even if all those bugs were addressed, there are inherent limitations in the plugin SDK, a large of part of which is that plugins are trapped in an iframe, sometimes aren’t rendered at all (like if the field is hidden or in a collapsed fieldset), etc. They’re essentially second-tier citizens compared to the “native” UI of a record. ← Stefano, I think this is the part you can best address? I’m guessing this was a deliberate design decision made when the plugin system was introduced? Do you have any thoughts to share?
  • These limitations severely limit what could be a more extensible development environment on top of our editor UI, requiring the use of entirely separate UIs that interact with our API instead of the plugin SDK. I think this is fundamentally what nroth is getting at…? That the plugins seem a bit crippled because of the particular way they’re sandboxed and isolated?

Hello! :wave:

From an engineering perspective, designing a plugin system in a web context is no simple task, especially if you’re aiming to ensure security. It’s a topic that many companies have delved into deeply. If you’re inclined to explore it, I recommend this Figma article:

The crux of the issue is this paradox:

[…] we wanted to make sure plugins would be safe for users to run, so we knew we wouldn’t want to simply eval(PLUGIN_CODE). That is the quintessential definition of insecure! Yet, eval is essentially what running a plugin boils down to.

The TLDR is: currently, the best solution for permitting unsecured third-party code for network requests or creating some UI is to use iframes in conjunction with a postMessage() messaging system.

But iframe is a solution that comes with a staggering number of limitations.

Once we’ve come to terms with the fact that we — or anyone else operating in “userland” — can’t do anything about these limitations, what we can do is aim to provide an SDK that allows for an “acceptable” level of convenience to execute common operations.

Now, talking about the more concrete problems :slight_smile:

If the autosizer is not functioning, this is a bug and we need to fix it (btw, DONE!). However, we cannot allow a dropdown to “break out” from the iframe. It would be nice, but it’s simply not possibile.

This means that any select/dropdown with an overlay will always look horrendous if used in a field extension :smile: The component we provide is more suitable for other hooks, like custom pages, custom sidebars, where the vertical space is not so limited.

This doesn’t imply that one cannot contemplate introducing new hooks if this scenario is common! For instance, something that allows plugins to associate multiple actions with a field via a single “native” dropdown could be very easy to implement:

About field extensions

[…] iframes sometimes aren’t rendered at all (like if the field is hidden or in a collapsed fieldset)

The iframes related to the fields are not rendered… when the field itself is not rendered. If a fieldset is collapsed, and its internal fields are not visible, why should we render the plugins related to those fields? Their job is to extend the field… but the field is not there! I wouldn’t consider it a bug, but rather a consequence of the purpose for which field extensions were designed.

I feel similarly about the idea of supporting the concept of “hidden fields” (that is, present but invisible). What would be the purpose? It seems to me a workaround to have some custom code somehow linked to a field, even when the field itself is not present. If this is the need, field extensions are not the appropriate tool.

We should also always take into account the potential large-scale issues that certain decisions may cause. Some clients have hundreds or thousands of fields within a single record, and removing every field when you collapse a fieldset can also help to decrease the sluggishness.

If you want to manage the form in its entirety (ie. coordinate multiple fields), there are other hooks available, like the suggested form outlets. Are existing hooks inconvenient/unfeasible? Again, let’s define the specific use cases, and we can evaluate whether to extend their functionalities, or to introduce completely new ones!

To wrap up…

Certainly, I haven’t been exhaustive in covering all the topics discussed in the thread… but I was more interested in conveying the general philosophy and approach we used in designing our SDK, so
as to better guide future requests in a way that makes them more easily actionable.

There’s no need to burden existing hooks with more responsibilities than they already have. Let’s try to define what would be nice to have, and we can find/introduce clean and specific ways to get there (if the iframes allow us, of course!)

Sometimes, the cleanest solution to a problem may not necessarily involve the concept of plugins, but rather exploit other tools that we provide, such as webhooks.