Skip to content
38 changes: 9 additions & 29 deletions src/lib/components/listbox/listbox.component.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ContentChildren,
ChangeDetectorRef,
EventEmitter,
Input,
Output,
QueryList,
TemplateRef,
forwardRef
} from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Listbox, ListboxChangeEvent } from 'primeng/listbox';
import { PrimeTemplate, SharedModule } from 'primeng/api';
import { SharedModule } from 'primeng/api';
import { NgTemplateOutlet } from '@angular/common';

@Component({
Expand Down Expand Up @@ -41,22 +38,18 @@ import { NgTemplateOutlet } from '@angular/common';
(onFocus)="onFocus.emit($event)"
(onBlur)="onBlurHandler($event)"
>
<!-- project only templates with pTemplate so PrimeNG listbox will detect them -->
<ng-content select="ng-template[pTemplate]"></ng-content>

<!-- forward captured item template into p-listbox preserving context -->
<ng-template pTemplate="item" let-item>
@if (itemTpl) {
@if (itemTemplate) {
<ng-template pTemplate="item" let-item>
<ng-container
[ngTemplateOutlet]="itemTpl?.template"
[ngTemplateOutlet]="itemTemplate"
[ngTemplateOutletContext]="{ $implicit: item }"
></ng-container>
}
</ng-template>
</ng-template>
}
</p-listbox>
`
})
export class ExtraListboxComponent implements ControlValueAccessor, AfterContentInit {
export class ExtraListboxComponent implements ControlValueAccessor {
@Input() options: any[] = [];
@Input() optionLabel = 'label';
@Input() optionValue: string | undefined = undefined;
Expand All @@ -69,14 +62,13 @@ export class ExtraListboxComponent implements ControlValueAccessor, AfterContent
@Input() optionGroupChildren: string | undefined = undefined;
@Input() scrollHeight = '200px';
@Input() emptyMessage: string | undefined = undefined;
@Input() itemTemplate: TemplateRef<any> | null = null;

@Output() onFocus = new EventEmitter<FocusEvent>();
@Output() onBlur = new EventEmitter<FocusEvent>();

protected modelValue: any = null;

@ContentChildren(PrimeTemplate) templates!: QueryList<PrimeTemplate>;
itemTpl?: PrimeTemplate;

private _disabled = false;
private _onChange: (value: any) => void = () => {};
Expand All @@ -86,18 +78,6 @@ export class ExtraListboxComponent implements ControlValueAccessor, AfterContent
return this._disabled;
}

constructor(private cdr: ChangeDetectorRef) {}

ngAfterContentInit(): void {
this.templates.forEach((tpl) => {
switch (tpl.getType()) {
case 'item':
this.itemTpl = tpl;
break;
}
});
this.cdr.detectChanges();
}

onChangeHandler(event: ListboxChangeEvent): void {
// Обновляем внутреннее значение и уведомляем форму об изменении.
Expand Down
59 changes: 59 additions & 0 deletions src/lib/components/menu/menu.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component, Input, TemplateRef, ViewChild } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { Menu } from 'primeng/menu';
import { MenuItem, PrimeTemplate } from 'primeng/api';

export interface ExtraMenuModel extends MenuItem {
caption?: string;
}

@Component({
selector: 'extra-menu',
host: { style: 'display: contents' },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

для чего?

Copy link
Copy Markdown
Author

@khaliulin khaliulin Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AxyIX делает хост-элемент menu прозрачным для layout (flex/grid родителя его не видят). Без этого menu был бы блочным элементом, ломающим позиционирование.

standalone: true,
imports: [Menu, PrimeTemplate, NgTemplateOutlet],
template: `
<p-menu #menuRef [model]="model" [popup]="popup" [appendTo]="popup ? 'body' : null">
<ng-template pTemplate="item" let-item>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

опять же вопрос с кастомизациями. разработчикам не придется свои шаблоны передавать?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AxyIX добавлен itemTemplate для кастомизации элемента меню 7896268

@if (itemTemplate) {
<ng-container [ngTemplateOutlet]="itemTemplate"
[ngTemplateOutletContext]="{ $implicit: item }">
</ng-container>
} @else {
<a
class="p-menu-item-link"
role="menuitem"
tabindex="0"
[class.p-disabled]="item.disabled"
[attr.href]="item.url || null"
[attr.target]="item.target || null"
(click)="!item.disabled && item.command && item.command({ originalEvent: $event, item: item })"
>
@if (item.icon) {
<span [class]="item.icon + ' p-menu-item-icon'"></span>
}
@if ($any(item).caption) {
<div class="menu-item-label">
<span class="p-menu-item-label">{{ item.label }}</span>
<small class="menu-item-caption">{{ $any(item).caption }}</small>
</div>
} @else {
<span class="p-menu-item-label">{{ item.label }}</span>
}
</a>
}
</ng-template>
</p-menu>
`,
})
export class ExtraMenuComponent {
@ViewChild('menuRef') menuRef!: Menu;

@Input() model: ExtraMenuModel[] = [];
@Input() popup = false;
@Input() itemTemplate: TemplateRef<any> | null = null;

toggle(event: Event): void {
this.menuRef.toggle(event);
}
}
7 changes: 7 additions & 0 deletions src/lib/components/menu/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "public_api.ts"
}
}

4 changes: 4 additions & 0 deletions src/lib/components/menu/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './menu.component';



67 changes: 67 additions & 0 deletions src/lib/providers/prime-preset/tokens/components/menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export const menuCss = ({ dt }: { dt: (token: string) => string }): string => `
.p-menu.p-component {
padding: ${dt('menu.extend.paddingY')} ${dt('menu.extend.paddingX')};
}

.p-menu .p-menu-item-content .p-menu-item-link .p-menu-item-label {
font-family: ${dt('fonts.fontFamily.base')};
font-size: ${dt('fonts.fontSize.300')};
font-weight: ${dt('fonts.fontWeight.regular')};
line-height: ${dt('fonts.lineHeight.400')};
}

.p-menu .p-menu-item-content .menu-item-label {
display: flex;
flex-direction: column;
gap: ${dt('menu.extend.extItem.caption.gap')};
}

.p-menu .p-menu-item-content .menu-item-caption {
font-family: ${dt('fonts.fontFamily.base')};
font-size: ${dt('fonts.fontSize.200')};
font-weight: ${dt('fonts.fontWeight.regular')};
color: ${dt('menu.colorScheme.light.extend.extItem.caption.color')};
}

.p-menu .p-menu-item:not(.p-disabled) .p-menu-item-content:hover,
.p-menu .p-menu-item:not(.p-disabled) .p-menu-item-content:hover .p-menu-item-link,
.p-menu .p-menu-item:not(.p-disabled) .p-menu-item-content:hover .p-menu-item-label,
.p-menu .p-menu-item:not(.p-disabled) .p-menu-item-content:hover .p-menu-item-icon {
background: ${dt('menu.colorScheme.light.item.focusBackground')};
color: ${dt('menu.colorScheme.light.item.focusColor')};
}

.p-menu .p-menu-item.p-menuitem-checked > .p-menu-item-content,
.p-menu .p-menu-item.p-focus > .p-menu-item-content {
background: ${dt('menu.extend.extItem.activeBackground')};
color: ${dt('menu.extend.extItem.activeColor')};
}

.p-menu .p-menu-item.p-menuitem-checked > .p-menu-item-content .p-menu-item-link,
.p-menu .p-menu-item.p-menuitem-checked > .p-menu-item-content .p-menu-item-label,
.p-menu .p-menu-item.p-focus > .p-menu-item-content .p-menu-item-link,
.p-menu .p-menu-item.p-focus > .p-menu-item-content .p-menu-item-label {
color: ${dt('menu.extend.extItem.activeColor')};
}

.p-menu .p-menu-item.p-menuitem-checked > .p-menu-item-content .p-menu-item-icon,
.p-menu .p-menu-item.p-focus > .p-menu-item-content .p-menu-item-icon {
color: ${dt('menu.colorScheme.light.extend.extItem.icon.activeColor')};
}

.p-menu .p-menu-item.p-menuitem-checked:not(.p-disabled) > .p-menu-item-content:hover {
background: ${dt('menu.colorScheme.light.item.focusBackground')};
color: ${dt('menu.colorScheme.light.item.focusColor')};
}

.p-menu .p-menu-item.p-menuitem-checked:not(.p-disabled) > .p-menu-item-content:hover .p-menu-item-icon {
color: ${dt('menu.colorScheme.light.item.focusColor')};
}

.p-menu .p-menu-submenu-label {
text-transform: uppercase;
font-size: ${dt('fonts.fontSize.200')};
font-family: ${dt('fonts.fontFamily.heading')};
line-height: ${dt('fonts.lineHeight.400')};
}
`;
2 changes: 1 addition & 1 deletion src/lib/providers/prime-preset/tokens/tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -3381,7 +3381,7 @@
},
"submenuLabel": {
"padding": "{navigation.submenuLabel.padding}",
"fontWeight": "{fonts.fontWeight.demibold}",
"fontWeight": "{fonts.fontWeight.regular}",
"background": "{navigation.submenuLabel.background}",
"color": "{navigation.submenuLabel.color}"
},
Expand Down
50 changes: 24 additions & 26 deletions src/stories/components/listbox/examples/listbox-custom.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { StoryObj } from '@storybook/angular';
import { ExtraListboxComponent } from '../../../../lib/components/listbox/listbox.component';
import { SharedModule } from 'primeng/api';

const options = [
{ name: 'Profile', description: 'Manage your account', icon: 'ti ti-user' },
Expand All @@ -11,24 +10,22 @@ const options = [
];

const template = `
<extra-listbox [formControl]="ctrl" [options]="options" optionLabel="name">
<ng-template pTemplate="item" let-item>
<i [class]="item.icon"></i>
<div class="p-listbox-option-label-group">
<span>{{ item.name }}</span>
<small class="p-listbox-option-caption">{{ item.description }}</small>
</div>
</ng-template>
</extra-listbox>
<extra-listbox [formControl]="ctrl" [options]="options" optionLabel="name" [itemTemplate]="customItem"></extra-listbox>

<ng-template #customItem let-item>
<i [class]="item.icon"></i>
<div class="p-listbox-option-label-group">
<span>{{ item.name }}</span>
<small class="p-listbox-option-caption">{{ item.description }}</small>
</div>
</ng-template>
`;
const styles = '';

@Component({
selector: 'app-listbox-custom',
standalone: true,
imports: [ExtraListboxComponent, SharedModule, ReactiveFormsModule],
imports: [ExtraListboxComponent, ReactiveFormsModule],
template,
styles,
})
export class ListboxCustomComponent {
ctrl = new FormControl(null);
Expand All @@ -42,29 +39,28 @@ export const Custom: StoryObj = {
parameters: {
controls: { disable: true },
docs: {
description: { story: 'Кастомный шаблон элемента с иконкой и подписью.' },
description: { story: 'Кастомный шаблон элемента с иконкой и подписью через `itemTemplate`.' },
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Listbox } from 'primeng/listbox';
import { SharedModule } from 'primeng/api';
import { ExtraListboxComponent } from '@cdek-it/angular-ui-kit';

@Component({
selector: 'app-listbox-custom',
standalone: true,
imports: [ExtraListboxComponent, SharedModule, ReactiveFormsModule],
imports: [ExtraListboxComponent, ReactiveFormsModule],
template: \`
<extra-listbox [formControl]="ctrl" [options]="options" optionLabel="name">
<ng-template pTemplate="item" let-item>
<i [class]="item.icon"></i>
<div class="p-listbox-option-label-group">
<span>{{ item.name }}</span>
<small class="p-listbox-option-caption">{{ item.description }}</small>
</div>
</ng-template>
</extra-listbox>
<extra-listbox [formControl]="ctrl" [options]="options" optionLabel="name" [itemTemplate]="customItem"></extra-listbox>

<ng-template #customItem let-item>
<i [class]="item.icon"></i>
<div class="p-listbox-option-label-group">
<span>{{ item.name }}</span>
<small class="p-listbox-option-caption">{{ item.description }}</small>
</div>
</ng-template>
\`,
})
export class ListboxCustomComponent {
Expand All @@ -80,3 +76,5 @@ export class ListboxCustomComponent {
},
},
};


Loading
Loading