Search code examples
openxmloffice-jsword-addinsword-contentcontrol

Word addin inserting complex list structure with styling in a contentControl using office.js done or not?


I'm trying to insert a complex list structure into a contentControl in MS Word using the javascript API. The structure is build according to an object that contains nested arrays: Different items containing sub items that contain different properties. These items arrays can change in size so it needs to be generic. Maybe the Office.js API isn't really build for what I am trying to achieve and I should be using either insertHTML (with the structure build in HTML) or OOXML.

This is the structure I already build

The function that produced this:

import ContentControl = Word.ContentControl;
import {formatDate} from '../formatters';
let firstItem = true;
let listId;

export async function resolveItems(contentControl: ContentControl, data: Array<any>, t) {
Word.run(  contentControl, async (context) => {
    contentControl.load('paragraphs');
    await context.sync();
    for (const item of data) {
        if (firstItem) {
            const firstPara = contentControl.paragraphs.getFirst();
            firstPara.insertText('ITEM (' + formatDate(item.date) + ')', 'Replace');
            firstItem = false;
            const contactList = firstPara.startNewList();
            contactList.load('id');
            await context.sync().catch((error) => {
                console.log('Error: ' + error);
                if (error instanceof OfficeExtension.Error) {
                    console.log('Debug info: ' + JSON.stringify(error.debugInfo));
                }
            });
            listId = contactList.id;
        } else {
            const otherItem = contentControl.insertParagraph('ITEM (' + formatDate(item.date) + ')', 'End');
            otherItem.load(['isListItem']);
            await context.sync();
            otherItem.attachToList(listId, 0);
        }
        for (const subItem of item.subItems) {
            let descriptionParaList = new Array();
            let subItemPara = contentControl.insertParagraph(subItem.title + ' (' + formatDate(subItem.date) + ')', 'End');
            subItemPara.load(['isListItem']);
            await context.sync();
            if (subItemPara.isListItem) {
                subItemPara.listItem.level = 1;
            } else {
                subItemPara.attachToList(listId, 1);
            }
            let descriptions = subItem.descriptions;
            for (const description of descriptions) {
                let descriptionPara = contentControl.insertParagraph('', 'End');
                descriptionPara.insertText(t(description.descriptionType) + ': ', 'Start').font.set({
                    italic: true
                });
                descriptionPara.insertText(description.description, 'End').font.set({
                    italic: false
                });
                descriptionPara.load(['isListItem', 'leftIndent']);
                descriptionParaList.push(descriptionPara);
            }
            await context.sync();
            for (const subItemPara of descriptionParaList) {
                if (subItemPara.isListItem) {
                    subItemPara.detachFromList();
                }
                subItemPara.leftIndent = 72;
            }

        }
    }
    return context.sync().catch((error) => {
        console.log('Error: ' + error);
        if (error instanceof OfficeExtension.Error) {
            console.log('Debug info: ' + JSON.stringify(error.debugInfo));
        }
    });
});}

The data structure looks like this:

'LAST_5_Items': [
    {
        'date': '2019-03-14T14:51:29.506+01:00',
        'type': 'ITEM',
        'subItems': [
            {
                'date': '2019-03-14T14:51:29.506+01:00',
                'title': 'SUBITEM 1',
                'descriptions': [
                    {
                        'descriptionType': 'REASON',
                        'description': 'Reason 1',
                        'additionalInformation': ''
                    }
                ]
            },
            {
                'date': '2019-03-14T14:51:29.506+01:00',
                'title': 'SUBITEM 2',
                'descriptions': [
                    {
                        'descriptionType': 'REASON',
                        'description': 'Reason 1',
                        'additionalInformation': ''
                    }
                ]
            }
        ]
    },
    {
        'date': '2019-03-14T14:16:26.220+01:00',
        'type': 'ITEM',
        'subItems': [
            {
                'date': '2019-03-14T14:16:26.220+01:00',
                'title': 'SUBITEM 1',
                'descriptions': [
                    {
                        'descriptionType': 'REASON',
                        'description': 'Reason 1',
                        'additionalInformation': ''
                    }
                ]
            },
            {
                'date': '2019-03-14T14:16:26.220+01:00',
                'title': 'SUBITEM 2',
                'descriptions': [
                    {
                        'descriptionType': 'REASON',
                        'description': 'Reason 1',
                        'additionalInformation': ''
                    },
                    {
                        'descriptionType': 'SUBJECTIVE',
                        'description': 'Subjective 1',
                        'additionalInformation': ''
                    },
                    {
                        'descriptionType': 'SUBJECTIVE',
                        'description': 'Subjective 2',
                        'additionalInformation': ''
                    },
                    {
                        'descriptionType': 'OBJECTIVE',
                        'description': 'Objective 1',
                        'additionalInformation': ''
                    },
                    {
                        'descriptionType': 'OBJECTIVE',
                        'description': 'Objective 2',
                        'additionalInformation': ''
                    },
                    {
                        'descriptionType': 'EVALUATION',
                        'description': 'Evaluation',
                        'additionalInformation': ''
                    },
                    {
                        'descriptionType': 'REASON',
                        'description': 'Reason 1',
                        'additionalInformation': ''
                    }
                ]
            }
        ]
    }
]

What I am trying to achieve is a template resolver. The addin will allow the user to put placeholder (ContentControls), tags like First name, Last name,... and the 5 last contacts (the one I am now describing) in the document and when he resolves the file it will fetch all the data needed and start replacing the ContentControls with this structured layout.

Yes the code works, but I highly doubt that the code is well structured with so many context.sync() calls. It is too slow and it is not usable. I need so many context.sync() because I need the properties of the List ID and if a Paragraph belongs to a list. Is there a better way to achieve what I am trying to achieve using the office.js API?

Ideally the queue should be synced once so the user would not see the content being added and changed in a very strange way like it is now behaving.

Thanks


Solution

  • There is a technique for avoiding having context.sync inside a loop. The basic idea is that you have two loops, with a single context.sync in between them. In the first loop, you create an array of objects. Each object contains a reference to one of the Office objects that you want to process (e.g., change, delete, etc.) It looks like paragraphs in your case. But the object has other properties. Each of them holds some item of data that you need to process the object. These other items may be properties of other Office objects or they may be other data. Also, either in your loop or just after it, you load all the properties you're going to need of all the Office objects in the objects in your array. (This is usually easier to do inside the loop.)

    Then you have context.sync.

    After the sync, you loop through the array of objects that you created before the context.sync. The code inside this second loop processes the first item in each object (which is the Office object that you want to process) using the other properties of the object you created.

    Here are a couple of examples of this technique:

    My answer to this StackOverflow question: Document not in sync after replace text.

    This code sample: https://github.com/OfficeDev/Word-Add-in-Angular2-StyleChecker/blob/master/app/services/word-document/word.document.service.js.