Replacing content in an existing ContentControl with a Plugin

Hello, I’ve been trying to replace the content inside an existing content control using the Plugin API.
Here’s an example:
image
I’d like to replace the content control’s content with the actual employee name.

I have 2 cases I’d like to cover - one where I have to insert content from a remote URL docx document (essentially copying the content from one document into a specific place inside the document, marked using a content control) and another one where I simply replace the text inside.
The 1st case I’ve “hacked” together like

window.Asc.plugin.executeMethod(
    "GetAllContentControls",
    [],
    (returnValue) => {
      returnValue
        .forEach((control) => {
          const id = control.InternalId
          const key = control.Tag.slice(tagPrefix.length)
          const value = values[key]
          if (!value) return
            const cc  = {
              Props: {
                Id: control.Id,
                Lock: 3,
                Tag: control.Tag,
              },
              Url: "http://my-documents.com/document.docx",
            }
            window.Asc.plugin.executeMethod("InsertAndReplaceContentControls", [
              [cc],
            ])
        })

This however doesn’t do what I want - it instead creates a new content control, which is not what I want. I tried passing in an InternalId to the Props like so

            ...
            const cc  = {
              Props: {
                Id: control.Id,
                Lock: 3,
                Tag: control.Tag,
                InternalId: control.InternalId
              },
              Url: "http://my-documents.com/document.docx",
            }
            window.Asc.plugin.executeMethod("InsertAndReplaceContentControls", [
              [cc],
            ])

I thought that it would solve it, but instead I get a cryptic error
image

1st question - What’s the recommended way to replace content controls with content from another document?

The 2nd case uses a similar approach

window.Asc.plugin.executeMethod(
    "GetAllContentControls",
    [],
    (returnValue) => {
      returnValue
        .forEach((control) => {
          const id = control.InternalId
          const key = control.Tag.slice(tagPrefix.length)
          const value = values[key]
          if (!value) return
            window.Asc.plugin.executeMethod(
              "SelectContentControl",
              [id],
              () => {
                window.Asc.plugin.executeMethod("PasteText", [`${value}`])
              }
            )
        })

This approach seems hacky to me because of the selection & pasting – the replacing doesn’t happen atomically and can for example get messy if the editor gets closed.

2nd question - What other ways are available for replacing content control’s content without selecting and pasting text inside them?
3rd question - Can this be achieved using the Asc.plugin.callCommand method & the Document Builder API?
4th question - What are the differences between the Plugin API and the Document Builder API w/ the callCommand method?
5th question - How can I wrap a selection into a content control using Plugin API or the Document Builder API?

Hello @fr3fou
I believe you have to take a look at this plugin example: sdkjs-plugins/example_work_with_content_controls at master · ONLYOFFICE/sdkjs-plugins · GitHub
It contains ‘insert and replace’ feature and it works properly. I hope it will be useful.
As for your questions:

What’s the recommended way to replace content controls with content from another document?

You can use InsertAndReplaceContentControls method as you already did it.

What other ways are available for replacing content control’s content without selecting and pasting text inside them?

You can use the same method: ONLYOFFICE Api Documentation - InsertAndReplaceContentControls
You can specify internalID and change it by this step.

Can this be achieved using the Asc.plugin.callCommand method & the Document Builder API?

There’re no such methods for DocBuilder.

What are the differences between the Plugin API and the Document Builder API w/ the callCommand method?

The purpose of the builder is to create/modify files without the participation of a graphical interface (editor). The purpose of plugins is to extend the current functionality of the editor. There’re literally different features. You can face similar methods on the both sides, but they are not exactly identical.

How can I wrap a selection into a content control using Plugin API or the Document Builder API?

Please take a look at this method: ONLYOFFICE Api Documentation - AddContentControl
Also this example will be useful sdkjs-plugins/example_add_rich_text_content_control at develop · ONLYOFFICE/sdkjs-plugins · GitHub

1 Like

Thank you Alexandre very much for your reply.
Is there a reason the example_add_rich_text_content_control example is on the develop branch on GitHub but not on master yet?

Also regarding this:

You can use the same method: ONLYOFFICE Api Documentation - InsertAndReplaceContentControls
You can specify internalID and change it by this step.

I’d like a way to replace the text inside the ContentControl but also preserve any formatting that has been applied to it (e.g font size / font style (italic, bold, etc), color, etc). The way I’ve achieved that is by using SelectContentControl & PasteText but it feels hacky (as I outlined in the original post). Is there an alternative to way to do that?

Is there a reason the example_add_rich_text_content_control example is on the develop branch on GitHub but not on master yet?

We merge it to master from time to time. Anyway, the mentioned example works.

I’d like a way to replace the text inside the ContentControl but also preserve any formatting that has been applied to it (e.g font size / font style (italic, bold, etc), color, etc). The way I’ve achieved that is by using SelectContentControl & PasteText but it feels hacky (as I outlined in the original post). Is there an alternative to way to do that?

You can try to use internalID to specify content control and change its contents. Please check out this code example: sdkjs/word/api_plugins.js at 0ab8241d66b1040bffc4069aec0ca11a8f041010 · ONLYOFFICE/sdkjs · GitHub

* // Add new content control
     * var arrDocuments = [{
     *  "Props": {
     *       "Id": 100,
     *       "Tag": "CC_Tag",
     *       "Lock": 3
     *   },
     *   "Script": "var oParagraph = Api.CreateParagraph();oParagraph.AddText('Hello world!');Api.GetDocument().InsertContent([oParagraph]);"
     *}]
     * window.Asc.plugin.executeMethod("InsertAndReplaceContentControls", [arrDocuments]);
     *
     * // Change existed content control
     * var arrDocuments = [{
     *  "Props": {
     *       "InternalId": "2_803"
     *   },
     *   "Script": "var oParagraph = Api.CreateParagraph();oParagraph.AddText('New text');Api.GetDocument().InsertContent([oParagraph]);"
     *}]
     * window.Asc.plugin.executeMethod("InsertAndReplaceContentControls", [arrDocuments]);

It should keep your font\style, but please check it out and let us know if something goes wrong.

Hello, thank you for the response. I tried replacing the content controls as you described but encountered several issues:

image

Here’s an example document. The placeholders on the right are inline content controls, created using AddContentControl.

function addPlainTextElement(label) {
  window.Asc.plugin.executeMethod("AddContentControl", [
    2,
    {
      Lock: 3,
      Tag: encodePlainTextTag(label),
      Alias: `${label} (Plain Text)`,
      PlaceHolderText: `{{${label}}}`,
      Color: {
        R: 69,
        G: 132,
        B: 80,
      },
    },
  ])
}

I tried replacing the content control’s content like so:

  const values = { companyName: "maik", companyAddress: "address here" }
  window.Asc.plugin.executeMethod(
    "GetAllContentControls",
    [],
    (returnValue) => {
      if (!returnValue.length) {
        alert("No tags in document")
        this.executeCommand("close", "")
        return
      }

      const arrDocuments = returnValue
        .filter((control) => control.Tag.startsWith(plainTextPrefix))
        .flatMap((control) => {
          const key = control.Tag.slice(plainTextPrefix.length)
          const value = values[key]
          if (!value) return
          return [
            {
              Props: {
                InternalId: control.InternalId,
              },
              // language=JavaScript
              Script: `
                const oParagraph = Api.CreateParagraph();
                oParagraph.AddText("${value}");
                Api.GetDocument().InsertContent([oParagraph]); 
               `
                .replaceAll("\n", "")
                .trim(),
            },
          ]
        })
      window.Asc.plugin.executeMethod("InsertAndReplaceContentControls", [
        arrDocuments,
      ])
    }
  )

However, when calling this function, I don’t get the desired result:
output
As you can see, I get unwanted extra newlines after the content controls (the “Name”, “Address”, and “Name With Placeholder filled” lines should have stayed together with no newlines separating them). Another bug is that if one has entered replaced the placeholder text inside, the content doesn’t get replaced properly (the last line demonstrates that) - it the text gets appended to the end and the placeholder & content control is kept.

We’re checking the situation. I will update this thread when we have something to share.

1 Like

Hello,
I’ve since figured out my issue – I had to add 2 extra arguments to my InsertContent call.

                const oParagraph = Api.CreateParagraph();
                oParagraph.AddText("${value}");
                Api.GetDocument().InsertContent([oParagraph], true, {KeepTextOnly: true}); 

However, now I have another question, related to removing the Content Controls.
Is there a way to remove a Content Control, while preserving the actual content inside?
Currently, I’m using the method like so:

  window.Asc.plugin.executeMethod("GetAllContentControls", [],
    (returnValue) => {
      const dd = returnValue
        .filter((control) => cleanup.has(`${control.Tag.split(";")[0]};`))
        .map((control) => ({ InternalId: control.InternalId }))
      window.Asc.plugin.executeMethod("RemoveContentControls", [dd])
    }
  )

This removes the content controls and their content. I’d like to keep the content, but remove the content control. Any idea how I can do that?

Do I understand it right that the described above situation is solved?

Is there a way to remove a Content Control, while preserving the actual content inside?
Currently, I’m using the method like so:

  window.Asc.plugin.executeMethod("GetAllContentControls", [],
    (returnValue) => {
      const dd = returnValue
        .filter((control) => cleanup.has(`${control.Tag.split(";")[0]};`))
        .map((control) => ({ InternalId: control.InternalId }))
      window.Asc.plugin.executeMethod("RemoveContentControls", [dd])
    }
  )

This removes the content controls and their content. I’d like to keep the content, but remove the content control.

It’s a little bit strange. It should work properly: ONLYOFFICE Api Documentation - RemoveContentControl
Removes the currently selected content control retaining all its contents.

What is your Document server version?

Do I understand it right that the described above situation is solved?

Yes.

It’s a little bit strange. It should work properly: ONLYOFFICE Api Documentation - RemoveContentControl
Removes the currently selected content control retaining all its contents.

I’m talking about RemoveContentControls - ONLYOFFICE Api Documentation - RemoveContentControls

What is your Document server version?

7.2.2

We’re looking into it. I will update this thread when we have something to share.

Hello @fr3fou
Unfortunately, RemoveContentControls method doesn’t have this feature (removing content control retaining all its contents). But we’re working on it already and we have added your request to internal tracksystem (internal number - 62055).
Sorry for inconvenience.

Thanks for the reply, I’d like to mention that both the old and the new behaviour should be kept – sometimes users would like to remove the content controls while keeping the content, but sometimes they’d like to remove it :smiley:

1 Like

Agree, probably we will implement additional flag for new behavior for this method. Anyway, I will update this thread when we have something to share.

Hello, was this ever introduced? Running Developer Edition 8.2 and my previous workaround seems to have stopped working:

  // https://api.onlyoffice.com/plugin/executemethod/text/removecontentcontrol
  static async removeContentControl(internalId?: string): Promise<ContentControlParentPr> {
    return executeMethod('RemoveContentControl', internalId)
  }

  // bulkRemoveContentControl calls removeContentControl, while also keeping the content inside the content control
  static async bulkRemoveContentControls(internalIds: string[]) {
    return Promise.all(internalIds.map((internalId) => this.removeContentControl(internalId)))
  }

  // https://api.onlyoffice.com/plugin/executemethod/text/removecontentcontrols
  // removeContentControl removes the ContentControls _and_ their content. There's no way to disable that behavior.
  // https://forum.onlyoffice.com/t/replacing-content-in-an-existing-contentcontrol-with-a-plugin/4251/13
  static async removeContentControlsAndContent(internalIds: string[]): Promise<void> {
    return executeMethod(
      'RemoveContentControls',
      internalIds.map((id) => ({ InternalId: id })),
    )
  }
// ...

export async function executeMethod(name: string, ...params: unknown[]) {
  return new Promise<any>((resolve) => {
    window.Asc.plugin.executeMethod(name, [...params], (value) => {
      resolve(value)
    })
  })
}

I keep getting this error:

sdk-all.js:7563 Uncaught TypeError: Cannot read properties of undefined (reading 'ke')
    at y.ZD (sdk-all.js:7563:411)
    at y.ZD (sdk-all.js:8801:26)
    at y.ZD (sdk-all.js:9612:281)
    at y.I4b (sdk-all.js:10178:412)
    at y.Jqb (sdk-all.js:9608:443)
    at y.ul (sdk-all.js:9609:360)
    at h.ryf (sdk-all-min.js:1650:293)
    at a.asc_docs_api.pluginMethod_RemoveContentControl (sdk-all-min.js:1810:131)

The stack trace is here:

    y.ZD = function(a, b, e) {
        if (!(0 >= this.aa.length) && (0 !== e || a[b] && this === a[b].ta)) {
            var d = 0;
            switch (e) {
            case 0:
                d = a[b].vd;
                break;
            case 1:
                d = 0;
                break;
            case -1:
                d = this.aa.length - 1
            }
            var f = a;
            if (null !== a && !0 === a[b].jia)
                if (d < this.aa.length)
                    f = null,
                    e = 1;
                else if (0 < d)
                    d--,
                    f = null,
                    e = -1;
                else
                    return;
            this.Vd.Qa = Math.max(0, Math.min(this.aa.length - 1, d));
            this.qa && (this.qa = this.Vd.Qa);
            this.aa[d] && this.aa[d].ZD ? this.aa[d].ZD(f, b + 1, e) : this.aa[d].ke() // error is here

This corresponds to this bit of code

sdkjs/word/Editor/DocumentContent.js at ad6d546be0c4353da9c84e2e8fc8172774e4ac73 · ONLYOFFICE/sdkjs · GitHub.

Hello @fr3fou
As far as I understand, you have contacted colleagues of mine via Zendesk. Please avoid posting the same issue in multiple communication channels and wait for a reply via Zendesk.