PrimeNG 22.1.1 is out. It is a patch on top of 22.1.0, so there are no new components and no API changes, just the fixes that came in after the last release.

npm install [email protected]

Most of these came from users reporting them, so if you hit one of them and opened an issue, thank you. Below is what changed and, where it matters, what you can stop working around.

Table with lazy and stateKey together

If you used lazy and stateKey on the same table, the header came out empty. No columns at all, which is a strange one to look at because the rows render fine.

The reason was on our side: the code that assigns the columns was inside a branch that only ran for tables without a state key. A table with a state key went down the other path, and that path expected the columns to already be set. They never were. Both cases now share the same assignment before anything else happens, so this combination works:

<p-table [value]="products" [columns]="cols" [lazy]="true" stateKey="products-table" [paginator]="true" [rows]="10" (onLazyLoad)="load($event)"></p-table>

The other Table fix is related to the paginator. After you cleared a column filter, the paginator kept showing the old record count. We fixed a version of this in 22.1.0, but the same mistake was in two more places that the earlier fix did not cover: the count was only recalculated while the previous count was zero, and after the first load it never is. So every recalculation after that returned the number it already had. It counts properly now, in all three places.

Dialog and appendTo

appendTo takes an element, but Dialog was only really reading it to check whether it said self. Anything else, including an element you passed, ended up on document.body.

This works now:

<div #host class="relative">
    <p-dialog [(visible)]="visible" [appendTo]="host()" header="Details">
        <p>Content</p>
    </p-dialog>
</div>
export class DialogDemo {
    host = viewChild.required<ElementRef>('host');
    visible = signal(false);
}

One thing to check after upgrading. If you were moving the dialog element yourself because appendTo did not work, remove that code. The component moves the node now, and if both of you are moving it you will get a fight over the same element.

ConfirmDialog goes through the same path, so it behaves the same way.

DatePicker marking the control as touched

In a reactive form with a required date, an error message appeared for a moment while the picker was still open, before you had finished picking anything. The control was being marked as touched at the point of selection, before the value was written back, so for that moment the form saw a touched and empty control and any error styling bound to touched showed up.

It now waits until the overlay closes, which is closer to what people mean by finishing with a field:

form = new FormGroup({
    startDate: new FormControl<Date | null>(null, Validators.required)
});
<p-datepicker formControlName="startDate" />

@if (form.controls.startDate.touched && form.controls.startDate.invalid) {
    <small class="text-red-500">Start date is required.</small>
}

Inline pickers do not have an overlay, so they still mark the control as touched when you select a date.

Keyboard navigation stopped working when you used your own item template. You could move the focus with the arrow keys, but Enter did nothing.

Internally the Enter key handler looks for the anchor element that Menu renders for each item and clicks it. When you provide a template, your markup replaces that anchor, so the lookup found nothing and the click went to the row instead, where there is no handler. It now falls back to the item content wrapper, so this works with the keyboard:

<p-menu [model]="items">
    <ng-template #item let-item>
        <div class="flex items-center gap-2 px-3 py-2">
            <i [class]="item.icon"></i>
            <span>{{ item.label }}</span>
        </div>
    </ng-template>
</p-menu>

Carousel’s page input was not setting the starting page in v22. The code that reads the initial page was in a lifecycle step that no longer ran before the first render, so the carousel always started at the first item regardless of what you passed. It is scheduled explicitly now, so [page]="2" starts where you asked.

TreeSelect’s overlay was misaligned when virtualScroll was on, usually opening above the input instead of below it. The panel only had a max height, so its real height depended on its content, and the overlay was positioned against a box that then changed size once the scroller measured itself. With virtual scroll on, the panel now gets a fixed height and the position holds.

A cleanup pass, and what it found

The other half of this release is not a feature. We turned on a set of lint rules that had never been enforced on the codebase and worked through the result, which was about 3,600 errors. Most of it was what you would expect from that kind of pass: formatting, unused parameters, ordering. Boring, and worth doing, but not something you would write about.

A few of them were real bugs though, and that is the part I found interesting.

Three places had a typo in an environment check, comparing typeof against the value undefined rather than the string "undefined". Since typeof gives you a string back, that comparison is true every time and the check does nothing. These were in our documentation site rather than in the library, so no application was affected. Still, it is a good example of the type of thing that survives code review: it reads correctly, it does not crash, and it does nothing.

Two were in the library itself. In one, someone had written ?. on a value to say it might be missing, then immediately destructured properties off it, which throws when it is missing. The optional chaining looked like a guard but the next character undid it. In the other, a helper was declared as taking a non-null element while both callers passed something that can be null, with a ! added to make the type error go away. At the top of a tree that value is genuinely null, and the function used it on its first line.

The biggest single change was in DatePicker’s time constraint logic, a switch (true) with about twenty cases written to fall through on purpose. Six of them were duplicates that could never be reached on their own, because an identical condition was checked earlier in the chain. They only existed as landing points for the fall-through. Rewritten as a normal if/else chain, it is about half the length.

Behavior changes quietly in a rewrite like that, so before keeping it we ran the old and the new implementation side by side over 419,904 input combinations and compared the return values and the side effects. No differences. I would not have trusted the change without that, and I would suggest the same to anyone touching a block like this: the tests you have were written for the behavior you know about.

We also moved the unit tests from Karma to Vitest in this release. That is internal and does not change anything you install. It is the reason the diff for a small release looks so large.

Upgrading

From any 22.1.x it is just a version bump. Nothing to change in your code:

npm install [email protected]

Two things to look at afterwards. If you had a workaround for the Dialog appendTo problem, remove it now. And if you were suppressing the DatePicker validation error with a custom updateOn or your own touched check, you can drop that too.