Skip to content

Content Controls

A content control is what Word’s Developer ribbon calls a structured document tag, the w:sdt element. It is a named region of a document that a program can find and fill without knowing where it sits on the page, which is what makes it the natural building block for a template. Of the three form mechanisms UniOffice exposes it is the only one that can create every control type, hold formatted content, and be discovered through headers and footers.

Every control carries two names. SetTag sets the programmatic identifier you look the control up by; SetAlias sets the friendly name Word shows in its UI. Neither has to be unique, and only the tag is worth designing.

Control typeSetterWhat it holds
Plain textSetText(multiLine bool)Unformatted text. multiLine permits soft line breaks.
Rich textSetRichText()Anything a document body can: formatted paragraphs, tables, images.
PictureSetPicture()A single image.
Drop-down listSetDropDownList(items ...SdtListItem)One of the listed items, no typing.
Combo boxSetComboBox(items ...SdtListItem)A listed item or free-form typed text.
Date pickerSetDate(format string)A date, displayed with the given mask, for example "M/d/yyyy".

The type setters are mutually exclusive and each replaces whatever was set before, so the last one called wins. SdtListItem has a DisplayText shown to the user and a Value stored in the document; the two are independent and only DisplayText appears on the page.

Creating a control

doc := document.New()
defer doc.Close()

status := doc.AddStructuredDocumentTag()
status.SetTag("status")
status.SetAlias("Invoice Status")
status.SetDropDownList(
    document.SdtListItem{DisplayText: "Draft", Value: "draft"},
    document.SdtListItem{DisplayText: "Paid", Value: "paid"},
)
status.SetContentText("Draft")

for _, sdt := range doc.StructuredDocumentTags() {
    fmt.Println(sdt.Tag(), sdt.Alias(), sdt.Text())
}

SetContentText clears whatever the control held and replaces it with one paragraph holding one run, which is the right call for the simple types. For a rich-text control, build the content instead: AddParagraph and AddTable append to the control the same way the document-level calls append to the body, and the paragraph they return takes the usual run properties and numbering definitions.

Each new tag is given a document-unique numeric ID automatically, so there is no need to call SetID yourself.

Block and inline controls

A block-level control occupies its own place in the document flow. An inline one sits inside a paragraph next to ordinary runs, which is what you want for a short field embedded mid-sentence.

BlockInline
Created bydoc.AddStructuredDocumentTag(), cell.AddStructuredDocumentTag()para.AddStructuredDocumentTag()
Go typeStructuredDocumentTagInlineStructuredDocumentTag
Content callsAddParagraph, AddTable, Paragraphs, TablesAddRun, Runs
Found bydoc.StructuredDocumentTags()para.StructuredDocumentTags()

The two share the whole property surface: SetTag, SetAlias, SetLock, SetPlaceholder, SetShowingPlaceholder, SetTemporary, SetDataBinding, every type setter, and Type, Text, Clear, SetContentText.

Finding controls in an existing document

doc.StructuredDocumentTags() returns every block-level control, descending into tables, headers, footers and controls nested inside other controls. That is the call that lets a program fill in a template it has never seen: match on Tag(), then SetContentText the value. Inline controls are scoped to their paragraph and come back from para.StructuredDocumentTags() instead, which descends into hyperlinks, simple fields and nested inline tags.

This overlaps with the string-substitution approach in Templates and mail merge. The difference is durability: a {{NAME}} placeholder is ordinary text that a user can break by editing around it, while a content control keeps its identity through editing and can be locked against deletion.

Locking

SetLock takes an SdtLock and controls what a person opening the document in Word is allowed to do.

ValueEffect
SdtLockUnset (default)No locking.
SdtLockSdtLockedThe control cannot be deleted; its contents stay editable.
SdtLockContentLockedThe contents cannot be edited; the control can be deleted.
SdtLockSdtContentLockedNeither deleted nor edited.
SdtLockUnlockedExplicitly unlocked, which matters when overriding an inherited setting.

SdtLockSdtLocked is the one that makes a control usable as a template region: the shape of the document is fixed and only the values can change.

Limitations

Word only permits block content inside rich-text and group controls. Calling AddTable or adding several paragraphs to a plain text, date, combo box or drop-down control produces a file Word will not open cleanly, and UniOffice does not check for it. Call SetRichText before AddTable.

There is no API for selecting a list item by its Value. SetContentText writes text into the control’s content, and matching that text to a listItem is left to Word. The Value half of an SdtListItem is only ever written into the item list.

SetPlaceholder writes a reference to a glossary document part by name. The library does not create that part, so nothing in the saved package defines the placeholder text. What a reader actually sees when SetShowingPlaceholder(true) is set is the control’s own current content, which SetContentText supplies.

Type() returns an SdtType, which has no String method, so %v prints the numeric constant rather than a name. SdtTypeGroup, SdtTypeEquation, SdtTypeCitation, SdtTypeBibliography, SdtTypeDocPartObj and SdtTypeDocPartList can all come back from a document Word produced, but UniOffice has no setter for any of them.

InlineStructuredDocumentTag has no AddParagraph or AddTable. Inline controls hold runs and nothing else, which follows from where they sit in the document.

Clear on a block control drops all of its content, tables included, and SetContentText calls Clear first.

Run the example

The example builds an invoice template: a plain text control for the customer name, a locked rich-text control holding italic terms, a date picker, a drop-down for status, a combo box for sales region, a rich-text control holding a bordered table of order items, another holding a bulleted list, and an inline control embedded in a sentence. It then prints what StructuredDocumentTags() and Paragraph.StructuredDocumentTags() each find.

git clone https://github.com/unidoc/unioffice-examples.git
cd unioffice-examples/document/structured-document-tags
go run main.go

If this is your first time using UniOffice, follow the getting started guide to create an API key and set up your development environment.

View the full source

Sample output

Block-level structured document tags:
- tag="customer_name" alias="Customer Name" type=2 text="Click here to enter the customer's name."
- tag="terms" alias="Terms And Conditions" type=1 text="Payment is due within 30 days of the invoice date."
- tag="invoice_date" alias="Invoice Date" type=6 text="1/1/2026"
- tag="status" alias="Status" type=5 text="Draft"
- tag="region" alias="Sales Region" type=4 text="North America"
- tag="order_items" alias="Order Items" type=1 text="ItemQuantityWidget4Gadget2"
- tag="delivery_options" alias="Delivery Options" type=1 text="Standard shippingExpress shippingLocal pickup"

Inline structured document tags:
- tag="status_inline" type=2 text="Draft"

Text() concatenates every run it can reach with no separator, which is why the table control reads as one unbroken string.

Last updated on