forty-cdk
llms.txt
Guides

Wrapping non-form roots

Design systems built on forty-cdk rarely expose the raw primitives. They wrap each one in a styled component carrying the system's selector and classes. For form-value controls that story lives in Wrapping form primitives, which owns the Signal Forms contract, the FOR_*_HOST_DIRECTIVE_INPUTS name tuples, and the [formField] discovery rules.

This guide covers everything else: the composed roots with no form value, namely Accordion, Dialog, Drawer, Popover, Tooltip, HoverCard, the menu family, Tabs, Stepper, Tree, Table, Carousel, ScrollArea, Toolbar, Pagination, Progress, Meter, Avatar, Fieldset, FileUpload, and the drag-drop / virtualization layers. The mechanics are simpler than the form side (there is no value contract to preserve) but they have one sharp edge that bites every wrapper on its first day, and it is the same edge: Angular does not inherit a directive's providers.

Subclassing is the default pattern here

A subclass with its own decorator inherits the primitive's inputs, outputs, host bindings, and listeners. Nothing needs re-declaring:

import { Directive } from '@angular/core';
import { ForAccordion } from 'forty-cdk/accordion';

@Directive({
  selector: '[myAccordion]',
  host: { class: 'my-accordion' },
})
export class MyAccordion extends ForAccordion {}

hostDirectives, the other pattern the form guide documents, is available too, but it is a worse fit for a composed root and there are no name tuples to help you: FOR_*_HOST_DIRECTIVE_INPUTS is a form-control artefact (it exists because an unbound value / touched name fails silently under [formField]), and no non-form primitive ships one. Re-exposing an accordion's or a dialog's whole surface through hostDirectives means hand-maintaining that list with nothing to check it against, so prefer the subclass and reach for hostDirectives only when your wrapper must also extend a different base class.

Re-provide the context token — this is the one that breaks

A composed primitive coordinates its pieces through an InjectionToken its root provides:

@Directive({
  selector: '[forDisclosure]',
  providers: [{ provide: FOR_DISCLOSURE_CONTEXT, useExisting: ForDisclosure }],
})
export class ForDisclosure { … }

Angular inherits compiled metadata through the class hierarchy, but each decorator declares its own providers, replacing the parent's array wholesale. A bare subclass therefore ships with no context provider at all, and every projected piece ([forAccordionTrigger], [forDialogTitle], [forPopoverContent], [forTabsTrigger]) throws the primitive's orphan error the moment it tries to resolve. The failure is loud, but the cause is not obvious from the message, which is why it deserves its own section in both wrapping guides.

Point useExisting at the subclass:

import { Directive } from '@angular/core';
import { FOR_POPOVER_CONTEXT, ForPopover } from 'forty-cdk/popover';

@Directive({
  selector: '[myPopover]',
  exportAs: 'myPopover',
  host: { class: 'my-popover' },
  providers: [{ provide: FOR_POPOVER_CONTEXT, useExisting: MyPopover }],
})
export class MyPopover extends ForPopover {}

Every non-form root that needs a plain re-provide, and the token to name. ForTable is the one root not in this table, because it needs provideForTable() instead (see the section below).

RootToken to re-provide
ForAccordionFOR_ACCORDION_CONTEXT
ForAvatarFOR_AVATAR_CONTEXT
ForCalendarFOR_CALENDAR_CONTEXT
ForCarouselFOR_CAROUSEL_CONTEXT
ForContextMenu / ForDropdownMenu / ForMenuSubFOR_MENU_CONTEXT
ForDialogFOR_DIALOG_CONTEXT
ForDisclosureFOR_DISCLOSURE_CONTEXT
ForDraggable / ForFreeDragFOR_DRAGGABLE_CONTEXT
ForDrawerFOR_DRAWER_CONTEXT
ForDropListFOR_DROP_LIST_CONTEXT
ForFieldFOR_FIELD_CONTEXT
ForFieldsetFOR_FIELDSET_CONTEXT
ForFileUploadFOR_FILE_UPLOAD_CONTEXT
ForHoverCardFOR_HOVER_CARD_CONTEXT
ForMenubarFOR_MENUBAR_CONTEXT
ForMeterFOR_METER_CONTEXT
ForNavigationMenuFOR_NAVIGATION_MENU_CONTEXT
ForPaginationFOR_PAGINATION_CONTEXT
ForPopoverFOR_POPOVER_CONTEXT
ForProgressFOR_PROGRESS_CONTEXT
ForScrollAreaFOR_SCROLL_AREA_CONTEXT
ForStepperFOR_STEPPER_CONTEXT
ForTabsFOR_TABS_CONTEXT
ForToastFOR_TOAST_CONTEXT
ForToolbarFOR_TOOLBAR_CONTEXT
ForTooltipFOR_TOOLTIP_CONTEXT
ForTreeFOR_TREE_CONTEXT
ForVirtualViewportFOR_VIRTUAL_VIEWPORT_CONTEXT

ForTree also provides FOR_TREE_CONTAINER_CONTEXT, the container every root-level [forTreeItem] registers through, so a subclass re-provides both tokens with useExisting. With only the first one, the first root-level item throws FORCDK-TREE-002.

ForField also provides FOR_FIELD_ANCHOR_CONTEXT, the slot [forFieldAnchor] fills, so a subclass re-provides both tokens with useExisting or the overlay controls inside it stop seeing the field anchor.

ForDatePicker and ForDateRangePicker find their projected calendar through the same token, with contentChild(FOR_CALENDAR_CONTEXT), so the ForCalendar row above is also what lets a picker read a subclassed calendar's selections and focus its active cell. A wrapper that composes ForCalendar through hostDirectives is found the same way, because the host directive provides the token itself.

Several of these roots (Accordion, Avatar, Carousel, Dialog, Drawer, NavigationMenu, Popover, Stepper, Tabs, Toast, Tree) split their coordination surface in two: the public FOR_<PRIMITIVE>_CONTEXT above, and an internal interface carrying the piece-registration protocol that is deliberately not exported (#1399, #1524). That split is invisible to a wrapper (#1593): both surfaces are two typed views of the same token on the same object, so the one-line re-provide above installs all of it.

What that costs is a precondition worth stating: on those roots the token must be aliased to the root itself (useExisting pointing at the root's class or a subclass of it) because the pieces read it at the internal interface's type. A useValue carrying your own object satisfies the token's declared type and resolves, but has none of the registration protocol behind it; in dev mode the first piece to resolve rejects it with a [forty-cdk/<primitive>]-prefixed error naming this shape (#1669).

Intermediate pieces provide tokens too, and subclassing one has the same requirement: ForAccordionItem (FOR_ACCORDION_ITEM_CONTEXT), ForStepperItem (FOR_STEPPER_ITEM_CONTEXT), ForTableRow (FOR_TABLE_ROW_CONTEXT), ForTreeItem (FOR_TREE_ITEM_CONTEXT), ForTreeGroup (FOR_TREE_CONTAINER_CONTEXT), ForMenuGroup (FOR_MENU_GROUP_CONTEXT), ForMenuRadioGroup (FOR_MENU_RADIO_GROUP_CONTEXT), ForTreeNodeDrag (FOR_TREE_NODE_DRAG_CONTEXT).

ForTable needs its provider helper, not a hand-written provider

ForTable is the one root a hand-written provider list cannot wrap. Its piece-registration protocol is not a second view of the root at all: it is a separate provider (TableRegistry, reached through a token that lives in forty-cdk/core so forty-cdk/table-virtualization can register through it from a second entry point), and [forTable]'s own constructor injects it. A subclass whose providers name only the public token therefore fails to construct at all (NG0201), and neither the registry class nor its token can be written by name from outside the library. Spread the helper instead:

import { Directive } from '@angular/core';
import { ForTable, provideForTable } from 'forty-cdk/table';

@Directive({
  selector: '[myTable]',
  exportAs: 'myTable',
  providers: provideForTable(MyTable),
})
export class MyTable<T> extends ForTable<T> {}

<for-table-body> has a public provideForTableDefRegistry() for the same reachability reason, but it is not a subclassing helper. See Table for the scaffold wrapper shape it supports.

What a wrapper must not swallow

Wrapping the root is safe. Wrapping the pieces into a single opaque component is where design systems lose behaviour the primitives were built to give them:

  • Keep mount == open in the consumer's hands. Overlay content is presence-controlled by the consumer's @if. The library never applies [hidden], precisely so animate.enter / animate.leave work. A wrapper that renders the content unconditionally and toggles CSS display loses the focus trap / scroll-lock / dismissible-layer lifecycle, which all hang off the content directive's lifetime.
  • Re-expose exportAs, or the template API disappears. Consumers reach imperative methods through #ref="forPopover". A subclass declares its own exportAs; pick a name and document it.
  • Do not re-emit outputs by hand. A subclass inherits (dismiss), (escapeKeyDown), (openChange) and friends already. Declaring a same-named output() in the subclass shadows the inherited one and silently drops the library's emissions.
  • Leave the data-* hooks alone. data-state, data-side, data-orientation, data-highlighted and the boolean flags are the styling contract (Styling); a wrapper adds classes next to them rather than replacing them.
  • Forward dir rather than re-deriving it. The root already resolves the ambient writing direction and reflects it to the host; a wrapper that adds its own [attr.dir] fights it.

Choosing a pattern

SituationPattern
Styled wrapper around one rootsubclass + re-provide the context token
ForTablesubclass + spread provideForTable(MyRoot)
Wrapper that must extend a different base classhostDirectives, re-exposing the surface by hand
Form-value control (Switch, Select, Slider, Combobox, …)Wrapping form primitives