PrimeNG 22.1.0 is out. It is a maintenance release on top of 22.0, so there are no new components in it, but the list of fixes is long and two of them change what you can build rather than only what works.
npm install [email protected]
The peer range on @angular/core, @angular/cdk and the rest is ^22.1.0, so
Angular 22.1 or later is required. If you are still on Angular 22.0, upgrade
Angular first.
Styling now reaches inside Shadow DOM
This one comes first because it removes a category of workaround rather than fixing a bug.
Component styles were injected as <style> elements in the document head.
Components living inside a shadow root could not see them: shadow boundaries
block document stylesheets by design, so a PrimeNG component rendered inside a
web component came out unstyled. Teams shipping Angular Elements, or using
ViewEncapsulation.ShadowDom on a hosting component, had to hand-copy styles
across the boundary.
You do not have to call anything to get the fix. Every component extends a base that checks its own root node during construction, and when that root node is a shadow root it registers it:
// primeng/src/basecomponent/basecomponent.ts
private _shareStylesWithShadowRoot() {
if (isPlatformServer(this.platformId)) return;
const rootNode = this.$el?.getRootNode?.();
if (typeof ShadowRoot === 'undefined' || !(rootNode instanceof ShadowRoot)) return;
inject(DestroyRef).onDestroy(inject(UseStyle).addShadowRoot(rootNode));
}
Registration returns its own cleanup function, which is handed straight to
DestroyRef, so a root stops being tracked when the component that registered
it is destroyed.
Underneath, the UseStyle service keeps every injected style as a constructed
CSSStyleSheet and pushes it into adoptedStyleSheets on each tracked root.
Adoption is two-way over time: a root registered later picks up the sheets
already created, and a style created later is appended to every root already
tracked. Removing a style removes it from the document and from every root that
adopted it. Stylesheets a root already had are left alone, so adopting does not
trample a host page’s own styles.
Two details about what is adopted and what is not. Theme variables (the
primitive, semantic and global custom property blocks) are deliberately not
adopted. They are loaded with variables: true and stay in the document head,
because custom properties inherit through a shadow boundary on their own, so
copying them into each root would buy nothing. The second detail is that
constructed stylesheets drop @import, and for that reason the <style>
element in the head is kept alongside the constructed sheet rather than replaced
by it. The head copy is the one that still applies imported CSS.
The same release teaches DOM traversal about shadow boundaries. getParents now
steps from a shadow root to its host element instead of stopping at null, so
overlay positioning and scroll handling can see the real ancestor chain from
inside a shadow tree.
If you have been keeping a manual copy of the theme CSS to make Angular Elements look right, you can delete that copy now.
Tree drag and drop no longer breaks under a filter
Filtering a Tree does not narrow the nodes in place. It builds shallow copies
({ ...node }) of the nodes that matched and renders those, which is fine for
display, but drag and drop is a structural operation and it was moving the copy.
The original array was left as it was, so clearing the filter put everything
back where it started and the move looked like it had been undone.
22.1.0 keeps a WeakMap from each rendered copy back to the node it came from,
so a drop reaches the real value:
// Given a node from the filtered view, get the one in your data.
// Returns the node itself when it is not a copy.
const source = tree.getOriginalNode(node);
The node rows use this themselves. UITreeNode exposes originalNode and
originalSubNodes getters that resolve through the map, and the drop path moves
nodes in those arrays rather than in the rendered copies. onNodeDrop reports
dropNode as the original node too, so a handler receives something that exists
in your data.
The Tree suite covers the behaviour directly, including a case that drops a node and then clears the filter to check the move stuck:
✓ should apply a root move to the original value and keep it when the filter is cleared
✓ should refresh the filtered view after a drop
✓ should move a filtered child node out of its parent without duplication
✓ should not allow dropping a node into its own filtered descendant
There is a related timing fix. A drop’s accept callback may be invoked
asynchronously, after dragend has already cleared the drag state and reset the
drop point hover signals. The drag state and drop position are now captured at
drop time and passed into insertNodeOnDrop as arguments, which means a
deferred accept applies the drop it was called for rather than whatever state
is left over by the time it runs.
The same work fixed node duplication under virtual scrolling. With virtual
scroll the row index is a position in the flattened list rather than in the
sibling array, so both positions are now resolved against the sibling arrays.
Select and MultiSelect
The overlay lifecycle events here are not new. Select has emitted onShow and
onHide since 22.0, and MultiSelect emits onPanelShow and onPanelHide. The
names differ between the two components, so a handler cannot simply be copied
across:
<p-select [options]="cities" [(ngModel)]="selectedCity" (onShow)="onOverlayShow()" (onHide)="onOverlayHide()" />
<p-multiselect [options]="plans" [(ngModel)]="selectedPlans" (onPanelShow)="onOverlayShow()" (onPanelHide)="onOverlayHide()" />
What changed in 22.1.0 is the payload type. These outputs, along with Overlay’s
deprecated onAnimationStart and onAnimationDone, are now typed
MotionEvent & AnimationEvent, and only the MotionEvent half of that
intersection exists at runtime. The AnimationEvent half is kept for backward
compatibility and the doc comment says so. Code that reads event.animationName
or event.elapsedTime compiles and then finds undefined at runtime, so treat
those members as gone even though the type still offers them.
optionDisabled also changed on both components. It was input<string>() and
is now input<string | ((item: any) => boolean)>(), so you can pass a function
without casting around the type:
<p-multiselect [options]="plans" optionLabel="name" [optionDisabled]="isPlanUnavailable" [(ngModel)]="selectedPlans" />
The field name form keeps working unchanged. Both forms go through
resolveFieldData, which accepted a resolver function before this release as
well, so the runtime behaviour is the same and it is the type that caught up.
Select also picked up a scroll fix. In multiple mode, opening the overlay no
longer scrolls the list to the selected item, which was yanking the panel away
from where you were looking.
The rest of the fixes
Table filter buttons. filterButtonProps used to be an input with a default
object, so passing a partial value replaced the whole thing and the fields you
left out fell back to nothing. The default is now {} and a
computedFilterButtonProps signal merges what you pass over the built-in
defaults, so { popover: { clear: { outlined: false } } } changes only that
button.
Chip and InputTags. Chip’s visible state is now a linkedSignal keyed on
what the chip renders (label, icon, image) rather than a plain signal. Angular
re-binds views by position, so a chip that hid itself on remove stayed hidden
for whichever item slid into that slot, which is why removing several tags in
InputTags left gaps behind. Chip also stopped binding false as an inline style
value, which was flooding Angular 22.1 dev builds with NG0318 warnings.
Table paginator. After the data changed, totalRecords kept a stale count,
because the recalculation only ran when the previous total was zero and after
the first load it never was. It tracks the current value length now.
FilterService. notEquals returned false for an empty filter value, so an
empty filter excluded every row instead of matching all of them. It returns
true now.
InputMask. Two fixes: the constructor effect no longer calls
writeValue('') and wipes the bound value on re-initialization, and pasting
into a masked input now dispatches a proper input event rather than only
emitting onUnmaskedChange.
Table. table.clear() now clears filter input contents as well as filter
state, and table content no longer paints in front of a frozen column.
Chart. The chart types no longer break builds that run with
skipLibCheck: false.
Datepicker. Falling below minDate in the time picker no longer snaps the
time to 11:00.
Menu family components (ContextMenu, MegaMenu, Steps, Dock, Menubar, TieredMenu, PanelMenu) and CascadeSelect, TreeSelect, SelectButton and MeterGroup all received smaller correctness fixes. The release touched 60 of the library’s source directories across 78 commits, so the list above gives you the shape of it and not all of it.
Upgrading
If you are on 22.0.x, there are no API changes to make, so the upgrade is a version bump:
npm install [email protected]
After that there are a few things to check. If you were working around Shadow
DOM styling by duplicating CSS, remove the duplicate, because adoption happens
on its own now and a stale copy will fight the real stylesheet. If you built
anything on top of Tree drag and drop while a filter was active, that code was
probably compensating for the copy behaviour, and it can talk to the original
nodes through getOriginalNode instead. And if an overlay handler reads
AnimationEvent members off onShow, onHide, onPanelShow or onPanelHide,
move it to the MotionEvent properties while the old typing still compiles.