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 type | Setter | What it holds |
|---|---|---|
| Plain text | SetText(multiLine bool) | Unformatted text. multiLine permits soft line breaks. |
| Rich text | SetRichText() | Anything a document body can: formatted paragraphs, tables, images. |
| Picture | SetPicture() | A single image. |
| Drop-down list | SetDropDownList(items ...SdtListItem) | One of the listed items, no typing. |
| Combo box | SetComboBox(items ...SdtListItem) | A listed item or free-form typed text. |
| Date picker | SetDate(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.
| Block | Inline | |
|---|---|---|
| Created by | doc.AddStructuredDocumentTag(), cell.AddStructuredDocumentTag() | para.AddStructuredDocumentTag() |
| Go type | StructuredDocumentTag | InlineStructuredDocumentTag |
| Content calls | AddParagraph, AddTable, Paragraphs, Tables | AddRun, Runs |
| Found by | doc.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.
| Value | Effect |
|---|---|
SdtLockUnset (default) | No locking. |
SdtLockSdtLocked | The control cannot be deleted; its contents stay editable. |
SdtLockContentLocked | The contents cannot be edited; the control can be deleted. |
SdtLockSdtContentLocked | Neither deleted nor edited. |
SdtLockUnlocked | Explicitly 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.goIf 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.