Skip to content

docx_plus.controls.builder

Build content controls (SDTs) — text, dropdown, date, checkbox. FormBuilder wraps a python-docx Document and emits valid w:sdt blocks that round-trip through Word.

Architecture walkthrough: ARCHITECTURE.md §6.

docx_plus.controls.builder

Build Word content controls (SDTs) — text, dropdown, date, checkbox.

python-docx stops at the paragraph/run layer; content controls are w:sdt elements that have to be synthesised at the lxml level. :class:FormBuilder wraps a python-docx :class:~docx.document.Document and provides add_* methods that emit valid w:sdt blocks and append them inline to a paragraph.

The builder handles the three failure modes the docx-forms skill prototype identified:

  1. w:id collisions — every id flows through :class:IdRegistry.
  2. The latent PlaceholderText style — materialised on construction so the grey placeholder text actually renders.
  3. w14 namespace declaration on the document root — required by w14:checkbox; verified at construction time.

This module imports only from docx_plus.core (SPEC §9.1). The PlaceholderText style definition is duplicated here intentionally rather than reused from :mod:docx_plus.styles.modify.

DropdownItem module-attribute

DropdownItem = str | tuple[str, str]

FormBuilder

FormBuilder(
    document_or_path: Document | str | PathLike[str] | None = None,
    *,
    id_registry: IdRegistry | None = None,
)

Wrap a python-docx Document and add fillable content controls.

self.doc is the underlying :class:~docx.document.Document — use it for ordinary document construction (headings, paragraphs, tables). Use the add_* methods to drop content controls into paragraphs you have made.

Each add_* method appends the SDT inline at the end of the paragraph you pass, so put the field's label in the paragraph text first.

Open or wrap a document and prepare the builder state.

Parameters:

Name Type Description Default
document_or_path Document | str | PathLike[str] | None

An open :class:~docx.document.Document, a path to a .docx file to open, or None to start a blank document.

None
id_registry IdRegistry | None

An existing :class:IdRegistry to share with other builders. None (default) creates a fresh registry seeded from the document's existing SDT ids.

None

Raises:

Type Description
MissingNamespaceError

If the document root does not declare the w14 namespace (required by w14:checkbox). Fresh python-docx documents always declare it.

Source code in docx_plus/controls/builder.py
def __init__(
    self,
    document_or_path: DocxDocument | str | os.PathLike[str] | None = None,
    *,
    id_registry: IdRegistry | None = None,
) -> None:
    """Open or wrap a document and prepare the builder state.

    Args:
        document_or_path: An open :class:`~docx.document.Document`, a path
            to a ``.docx`` file to open, or ``None`` to start a blank
            document.
        id_registry: An existing :class:`IdRegistry` to share with other
            builders. ``None`` (default) creates a fresh registry seeded
            from the document's existing SDT ids.

    Raises:
        MissingNamespaceError: If the document root does not declare the
            ``w14`` namespace (required by ``w14:checkbox``). Fresh
            python-docx documents always declare it.
    """
    if document_or_path is None:
        self.doc = Document()
    elif isinstance(document_or_path, (str, os.PathLike)):
        self.doc = Document(os.fspath(document_or_path))
    else:
        self.doc = document_or_path

    self._id_registry = id_registry if id_registry is not None else IdRegistry(self.doc)
    _verify_w14_declared(self.doc)
    _ensure_placeholder_style(self.doc)

add_text_control

add_text_control(
    paragraph: Paragraph,
    *,
    tag: str,
    alias: str | None = None,
    placeholder: str = "Click to enter text",
    multiline: bool = False,
) -> etree._Element

Append an inline plain-text content control to paragraph.

Parameters:

Name Type Description Default
paragraph Paragraph

The python-docx paragraph to append into.

required
tag str

Stable machine-readable identifier for the control.

required
alias str | None

Optional human-friendly label shown in Word's UI.

None
placeholder str

The grey "click here" prompt rendered inside the empty control.

'Click to enter text'
multiline bool

If True, allow hard line breaks inside the control (use for addresses, comment boxes).

False

Returns:

Type Description
_Element

The created w:sdt element.

Source code in docx_plus/controls/builder.py
def add_text_control(
    self,
    paragraph: Paragraph,
    *,
    tag: str,
    alias: str | None = None,
    placeholder: str = "Click to enter text",
    multiline: bool = False,
) -> etree._Element:
    """Append an inline plain-text content control to ``paragraph``.

    Args:
        paragraph: The python-docx paragraph to append into.
        tag: Stable machine-readable identifier for the control.
        alias: Optional human-friendly label shown in Word's UI.
        placeholder: The grey "click here" prompt rendered inside the
            empty control.
        multiline: If ``True``, allow hard line breaks inside the control
            (use for addresses, comment boxes).

    Returns:
        The created ``w:sdt`` element.
    """
    sdt, sdt_pr, sdt_content = self._new_sdt(tag=tag, alias=alias)
    sub(sdt_pr, "w:showingPlcHdr")
    text_attrs: dict[str, str] = {"w:multiLine": "1"} if multiline else {}
    sub(sdt_pr, "w:text", **text_attrs)

    sdt_content.append(_placeholder_run(placeholder))
    sdt.append(sdt_content)
    paragraph._p.append(sdt)
    return sdt

add_dropdown

add_dropdown(
    paragraph: Paragraph,
    *,
    tag: str,
    items: list[DropdownItem],
    alias: str | None = None,
    placeholder: str = "Choose an item",
    editable: bool = False,
) -> etree._Element

Append a dropdown (or combobox) content control to paragraph.

Parameters:

Name Type Description Default
paragraph Paragraph

The python-docx paragraph to append into.

required
tag str

Stable machine-readable identifier for the control.

required
items list[DropdownItem]

A list of either plain strings, or (display, value) tuples when the stored value should differ from the shown label.

required
alias str | None

Optional human-friendly label shown in Word's UI.

None
placeholder str

The "Choose an item" prompt rendered inside the empty control. A placeholder list-item with empty value is also added as the first dropdown entry.

'Choose an item'
editable bool

If True, produce a w:comboBox (user may type a value not in the list) instead of a w:dropDownList.

False

Returns:

Type Description
_Element

The created w:sdt element.

Raises:

Type Description
TypeError

If items contains anything that is not a string or a 2-tuple of strings.

Source code in docx_plus/controls/builder.py
def add_dropdown(
    self,
    paragraph: Paragraph,
    *,
    tag: str,
    items: list[DropdownItem],
    alias: str | None = None,
    placeholder: str = "Choose an item",
    editable: bool = False,
) -> etree._Element:
    """Append a dropdown (or combobox) content control to ``paragraph``.

    Args:
        paragraph: The python-docx paragraph to append into.
        tag: Stable machine-readable identifier for the control.
        items: A list of either plain strings, or ``(display, value)``
            tuples when the stored value should differ from the shown
            label.
        alias: Optional human-friendly label shown in Word's UI.
        placeholder: The "Choose an item" prompt rendered inside the
            empty control. A placeholder list-item with empty value is
            also added as the first dropdown entry.
        editable: If ``True``, produce a ``w:comboBox`` (user may type a
            value not in the list) instead of a ``w:dropDownList``.

    Returns:
        The created ``w:sdt`` element.

    Raises:
        TypeError: If ``items`` contains anything that is not a string
            or a 2-tuple of strings.
    """
    sdt, sdt_pr, sdt_content = self._new_sdt(tag=tag, alias=alias)
    sub(sdt_pr, "w:showingPlcHdr")
    list_tag = "w:comboBox" if editable else "w:dropDownList"
    list_el = sub(sdt_pr, list_tag)

    sub(list_el, "w:listItem", **{"w:displayText": placeholder, "w:value": ""})
    for raw_item in items:
        display, value = _normalise_dropdown_item(raw_item)
        sub(list_el, "w:listItem", **{"w:displayText": display, "w:value": value})

    sdt_content.append(_placeholder_run(placeholder))
    sdt.append(sdt_content)
    paragraph._p.append(sdt)
    return sdt

add_date_picker

add_date_picker(
    paragraph: Paragraph,
    *,
    tag: str,
    alias: str | None = None,
    placeholder: str = "Click to select a date",
    date_format: str = "M/d/yyyy",
    lcid: str = "en-US",
) -> etree._Element

Append a date-picker content control to paragraph.

Parameters:

Name Type Description Default
paragraph Paragraph

The python-docx paragraph to append into.

required
tag str

Stable machine-readable identifier for the control.

required
alias str | None

Optional human-friendly label shown in Word's UI.

None
placeholder str

The grey "click here" prompt rendered inside the empty control.

'Click to select a date'
date_format str

Word's date-format string (e.g. "M/d/yyyy", "dddd, MMMM d, yyyy").

'M/d/yyyy'
lcid str

Locale identifier (BCP-47 form, e.g. "en-US").

'en-US'

Returns:

Type Description
_Element

The created w:sdt element.

Source code in docx_plus/controls/builder.py
def add_date_picker(
    self,
    paragraph: Paragraph,
    *,
    tag: str,
    alias: str | None = None,
    placeholder: str = "Click to select a date",
    date_format: str = "M/d/yyyy",
    lcid: str = "en-US",
) -> etree._Element:
    """Append a date-picker content control to ``paragraph``.

    Args:
        paragraph: The python-docx paragraph to append into.
        tag: Stable machine-readable identifier for the control.
        alias: Optional human-friendly label shown in Word's UI.
        placeholder: The grey "click here" prompt rendered inside the
            empty control.
        date_format: Word's date-format string (e.g. ``"M/d/yyyy"``,
            ``"dddd, MMMM d, yyyy"``).
        lcid: Locale identifier (BCP-47 form, e.g. ``"en-US"``).

    Returns:
        The created ``w:sdt`` element.
    """
    sdt, sdt_pr, sdt_content = self._new_sdt(tag=tag, alias=alias)
    sub(sdt_pr, "w:showingPlcHdr")
    date_el = sub(sdt_pr, "w:date")
    sub(date_el, "w:dateFormat", **{"w:val": date_format})
    sub(date_el, "w:lid", **{"w:val": lcid})
    sub(date_el, "w:storeMappedDataAs", **{"w:val": "dateTime"})
    sub(date_el, "w:calendar", **{"w:val": "gregorian"})

    sdt_content.append(_placeholder_run(placeholder))
    sdt.append(sdt_content)
    paragraph._p.append(sdt)
    return sdt

add_checkbox

add_checkbox(
    paragraph: Paragraph,
    *,
    tag: str,
    alias: str | None = None,
    checked: bool = False,
) -> etree._Element

Append a Word 2010+ w14:checkbox content control to paragraph.

The visible glyph and the w14:checked flag are kept in sync, so the box renders correctly even before Word ever opens the file.

Parameters:

Name Type Description Default
paragraph Paragraph

The python-docx paragraph to append into.

required
tag str

Stable machine-readable identifier for the control.

required
alias str | None

Optional human-friendly label shown in Word's UI.

None
checked bool

Initial checked state.

False

Returns:

Type Description
_Element

The created w:sdt element.

Source code in docx_plus/controls/builder.py
def add_checkbox(
    self,
    paragraph: Paragraph,
    *,
    tag: str,
    alias: str | None = None,
    checked: bool = False,
) -> etree._Element:
    """Append a Word 2010+ ``w14:checkbox`` content control to ``paragraph``.

    The visible glyph and the ``w14:checked`` flag are kept in sync, so
    the box renders correctly even before Word ever opens the file.

    Args:
        paragraph: The python-docx paragraph to append into.
        tag: Stable machine-readable identifier for the control.
        alias: Optional human-friendly label shown in Word's UI.
        checked: Initial checked state.

    Returns:
        The created ``w:sdt`` element.
    """
    sdt, sdt_pr, sdt_content = self._new_sdt(tag=tag, alias=alias)
    checkbox = sub(sdt_pr, "w14:checkbox")
    sub(checkbox, "w14:checked", **{"w14:val": "1" if checked else "0"})
    sub(
        checkbox,
        "w14:checkedState",
        **{"w14:val": _CHECKBOX_CHECKED_HEX, "w14:font": _CHECKBOX_FONT},
    )
    sub(
        checkbox,
        "w14:uncheckedState",
        **{"w14:val": _CHECKBOX_UNCHECKED_HEX, "w14:font": _CHECKBOX_FONT},
    )

    sdt_content.append(
        _checkbox_glyph_run(
            _CHECKBOX_CHECKED_GLYPH if checked else _CHECKBOX_UNCHECKED_GLYPH,
        ),
    )
    sdt.append(sdt_content)
    paragraph._p.append(sdt)
    return sdt

save

save(path: str | PathLike[str]) -> str

Save the wrapped document to path and return the path as a string.

Source code in docx_plus/controls/builder.py
def save(self, path: str | os.PathLike[str]) -> str:
    """Save the wrapped document to ``path`` and return the path as a string."""
    self.doc.save(os.fspath(path))
    return os.fspath(path)

MissingNamespaceError

Bases: DocxPlusError

Raised when a required namespace is not declared on the document root.

InvalidDropdownItemError

Bases: DocxPlusError, TypeError

Raised when a dropdown items entry is not a str or (str, str).

Subclasses TypeError so existing except TypeError: clauses still catch it; also subclasses :class:DocxPlusError per SPEC §9.7.