Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions apps/showcase/doc/splitbutton/icons-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,63 @@ import { AppCode } from '@/components/doc/app.code';
import { AppDocSectionText } from '@/components/doc/app.docsectiontext';
import { SplitButtonModule } from 'primeng/splitbutton';
import { ToastModule } from 'primeng/toast';
import { RouterModule } from '@angular/router';

@Component({
selector: 'icons-doc',
standalone: true,
imports: [AppCode, AppDocSectionText, SplitButtonModule, ToastModule],
imports: [AppCode, AppDocSectionText, SplitButtonModule, ToastModule, RouterModule],
template: `
<app-docsectiontext>
<p>The buttons and menuitems have support to display icons.</p>
<p>The buttons and menuitems have support to display icons with CSS classes such as PrimeIcons.</p>
</app-docsectiontext>
<div class="card flex justify-center">
<p-toast />
<p-splitbutton label="Save" icon="pi pi-check" dropdownIcon="pi pi-cog" [model]="items" />
</div>
<app-code></app-code>

<app-docsectiontext>
<p>
Custom icons from any library are supported through the <i>icon</i>, <i>dropdownicon</i>, and <i>item</i> templates, for example inline SVG, Font Awesome, Material Icons, or images. When using a template, omit the corresponding
<i>icon</i> or <i>dropdownIcon</i> property. Visit the <a routerLink="/customicons">custom icons</a> documentation for more examples.
</p>
</app-docsectiontext>
<div class="card flex justify-center">
<p-toast />
<p-splitbutton label="Save" [model]="templateItems" (onClick)="save()">
<ng-template #icon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1rem" height="1rem" aria-hidden="true">
<path fill="currentColor" d="M9,16.17,4.83,12,3.41,13.41,9,19,21,7,19.59,5.59Z" />
</svg>
</ng-template>
<ng-template #dropdownicon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1rem" height="1rem" aria-hidden="true">
<path fill="currentColor" d="M12,15.25a.74.74,0,0,1-.53-.22l-5-5A.75.75,0,0,1,7.53,9L12,13.44,16.47,9A.75.75,0,0,1,17.53,10l-5,5A.74.74,0,0,1,12,15.25Z" />
</svg>
</ng-template>
<ng-template #item let-item>
<span class="flex items-center gap-2 px-3 py-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1rem" height="1rem" aria-hidden="true">
<path fill="currentColor" [attr.d]="itemIcons[item.label]" />
</svg>
<span>{{ item.label }}</span>
</span>
</ng-template>
</p-splitbutton>
</div>
<app-code></app-code>
`,
providers: [MessageService]
})
export class IconsDoc {
items: MenuItem[];
templateItems: MenuItem[];
itemIcons: Record<string, string> = {
Update: 'M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12H1L4.96,16.03L9,12H6A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L17.65,6.35M12,20A8,8 0 0,0 20,12H23L19.04,7.97L15,12H18A6,6 0 0,1 12,18C10.34,18 8.86,17.31 7.78,16.22L6.35,17.65C7.8,19.1 9.79,20 12,20Z',
Delete: 'M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z',
Quit: 'M16,17V14H9V10H16V7L21,12L16,17M14,2A2,2 0 0,1 16,4V6H14V4H5V20H14V18H16V20A2,2 0 0,1 14,22H5A2,2 0 0,1 3,20V4A2,2 0 0,1 5,2H14Z'
};

constructor(private messageService: MessageService) {
this.items = [
Expand Down Expand Up @@ -51,5 +89,31 @@ export class IconsDoc {
}
}
];

this.templateItems = [
{
label: 'Update',
command: () => {
this.messageService.add({ severity: 'success', summary: 'Updated', detail: 'Data Updated', life: 3000 });
}
},
{
label: 'Delete',
command: () => {
this.messageService.add({ severity: 'warn', summary: 'Delete', detail: 'Data Deleted', life: 3000 });
}
},
{ separator: true },
{
label: 'Quit',
command: () => {
window.open('https://angular.io/', '_blank');
}
}
];
}

save() {
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Data Saved', life: 3000 });
}
}
67 changes: 67 additions & 0 deletions packages/primeng/src/splitbutton/splitbutton.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,40 @@ class TestContentTemplateSplitButtonComponent {
model: MenuItem[] = [{ label: 'Template Action 1' }, { label: 'Template Action 2' }];
}

@Component({
standalone: false,
template: `
<p-splitbutton label="Save" [model]="model">
<ng-template #icon>
<svg class="custom-action-icon" data-testid="custom-action-icon"></svg>
</ng-template>
<ng-template #dropdownicon>
<svg class="custom-dropdown-svg" data-testid="custom-dropdown-svg"></svg>
</ng-template>
</p-splitbutton>
`
})
class TestIconTemplateSplitButtonComponent {
model: MenuItem[] = [{ label: 'Action 1' }];
}

@Component({
standalone: false,
template: `
<p-splitbutton label="Save" [model]="model">
<ng-template #item let-item>
<a class="custom-menu-item">
<svg class="custom-item-icon" [attr.data-item]="item.label"></svg>
<span>{{ item.label }}</span>
</a>
</ng-template>
</p-splitbutton>
`
})
class TestItemTemplateSplitButtonComponent {
model: MenuItem[] = [{ label: 'Update' }, { label: 'Delete' }];
}

// Severity SplitButton Test
@Component({
standalone: false,
Expand Down Expand Up @@ -308,6 +342,8 @@ describe('SplitButton', () => {
TestBasicSplitButtonComponent,
TestTemplateSplitButtonComponent,
TestContentTemplateSplitButtonComponent,
TestIconTemplateSplitButtonComponent,
TestItemTemplateSplitButtonComponent,
TestSeveritySplitButtonComponent,
TestSplitButtonVariantsComponent,
TestDisabledSplitButtonComponent,
Expand Down Expand Up @@ -1057,6 +1093,37 @@ describe('SplitButton', () => {
expect(() => splitButtonInstance.ngAfterContentInit()).not.toThrow();
expect(splitButtonInstance.templates).toBeDefined();
});

it('should render custom icon template on default button', async () => {
const iconTemplateFixture = TestBed.createComponent(TestIconTemplateSplitButtonComponent);
iconTemplateFixture.detectChanges();
await iconTemplateFixture.whenStable();

const splitButtonInstance = iconTemplateFixture.debugElement.query(By.directive(SplitButton)).componentInstance;
expect(splitButtonInstance.iconTemplate).toBeDefined();
expect(splitButtonInstance.hasIconTemplate).toBe(true);

const customActionIcon = iconTemplateFixture.debugElement.query(By.css('[data-testid="custom-action-icon"]'));
expect(customActionIcon).toBeTruthy();
});

it('should forward item template to tiered menu', async () => {
const itemTemplateFixture = TestBed.createComponent(TestItemTemplateSplitButtonComponent);
itemTemplateFixture.detectChanges();
await itemTemplateFixture.whenStable();

const splitButtonInstance = itemTemplateFixture.debugElement.query(By.directive(SplitButton)).componentInstance;
expect(splitButtonInstance.itemTemplate).toBeDefined();
expect(splitButtonInstance.menu?.itemTemplate).toBeDefined();

const buttons = itemTemplateFixture.debugElement.queryAll(By.css('button'));
buttons[1].nativeElement.click();
itemTemplateFixture.detectChanges();
await itemTemplateFixture.whenStable();

const customMenuItems = itemTemplateFixture.debugElement.queryAll(By.css('.custom-menu-item'));
expect(customMenuItems.length).toBeGreaterThan(0);
});
});
});

Expand Down
62 changes: 58 additions & 4 deletions packages/primeng/src/splitbutton/splitbutton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import { ChevronDownIcon } from 'primeng/icons';
import { Ripple } from 'primeng/ripple';
import { TieredMenu } from 'primeng/tieredmenu';
import { TooltipModule } from 'primeng/tooltip';
import type { ButtonIconTemplateContext } from 'primeng/types/button';
import { ButtonProps, MenuButtonProps, SplitButtonPassThrough } from 'primeng/types/splitbutton';
import type { TieredMenuItemTemplateContext } from 'primeng/types/tieredmenu';
import { SplitButtonStyle } from './style/splitbuttonstyle';

const SPLITBUTTON_INSTANCE = new InjectionToken<SplitButton>('SPLITBUTTON_INSTANCE');
Expand Down Expand Up @@ -84,7 +86,7 @@ type SplitButtonIconPosition = 'left' | 'right';
[text]="text"
[outlined]="outlined"
[size]="size"
[icon]="icon"
[icon]="icon && !hasIconTemplate ? icon : undefined"
[iconPos]="iconPos"
[label]="label"
(click)="onDefaultButtonClick($event)"
Expand All @@ -97,7 +99,13 @@ type SplitButtonIconPosition = 'left' | 'right';
[tooltipOptions]="tooltipOptions"
[pt]="ptm('pcButton')"
[unstyled]="unstyled()"
></button>
>
<ng-container *ngIf="!icon && hasIconTemplate">
<span [class]="defaultButtonIconClass" aria-hidden="true">
<ng-template *ngTemplateOutlet="iconTemplate || _iconTemplate; context: iconTemplateContext"></ng-template>
</span>
</ng-container>
</button>
</ng-template>
<button
type="button"
Expand Down Expand Up @@ -137,7 +145,11 @@ type SplitButtonIconPosition = 'left' | 'right';
(onShow)="onShow()"
[pt]="ptm('pcMenu')"
[unstyled]="unstyled()"
></p-tieredmenu>
>
<ng-template #item let-item let-hasSubmenu="hasSubmenu" *ngIf="itemTemplate || _itemTemplate">
<ng-container *ngTemplateOutlet="itemTemplate || _itemTemplate; context: { $implicit: item, hasSubmenu: hasSubmenu }"></ng-container>
</ng-template>
</p-tieredmenu>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [SplitButtonStyle, { provide: SPLITBUTTON_INSTANCE, useExisting: SplitButton }, { provide: PARENT_INSTANCE, useExisting: SplitButton }],
Expand Down Expand Up @@ -354,11 +366,21 @@ export class SplitButton extends BaseComponent<SplitButtonPassThrough> {
* @group Templates
*/
@ContentChild('content', { descendants: false }) contentTemplate: TemplateRef<void> | undefined;
/**
* Custom icon template for the default action button.
* @group Templates
*/
@ContentChild('icon', { descendants: false }) iconTemplate: TemplateRef<ButtonIconTemplateContext> | undefined;
/**
* Custom dropdown icon template.
* @group Templates
**/
@ContentChild('dropdownicon', { descendants: false }) dropdownIconTemplate: TemplateRef<void> | undefined;
/**
* Custom item template for the overlay menu.
* @group Templates
*/
@ContentChild('item', { descendants: false }) itemTemplate: TemplateRef<TieredMenuItemTemplateContext> | undefined;

@ContentChildren(PrimeTemplate) templates: QueryList<PrimeTemplate> | undefined;

Expand All @@ -372,10 +394,35 @@ export class SplitButton extends BaseComponent<SplitButtonPassThrough> {

_contentTemplate: TemplateRef<void> | undefined;

_iconTemplate: TemplateRef<ButtonIconTemplateContext> | undefined;

_dropdownIconTemplate: TemplateRef<void> | undefined;

_itemTemplate: TemplateRef<TieredMenuItemTemplateContext> | undefined;

$appendTo = computed(() => this.appendTo() || this.config.overlayAppendTo());

get hasIconTemplate(): boolean {
return !!(this.iconTemplate || this._iconTemplate);
}

get defaultButtonIconClass(): string {
const classes = ['p-button-icon'];

if (this.label && this.iconPos) {
classes.push(`p-button-icon-${this.iconPos}`);
}

return classes.join(' ');
}

get iconTemplateContext(): ButtonIconTemplateContext {
return {
class: this.defaultButtonIconClass,
pt: this.ptm('pcButton')?.icon
};
}

onInit() {
this.ariaId = uuid('pn_id_');
}
Expand All @@ -387,12 +434,19 @@ export class SplitButton extends BaseComponent<SplitButtonPassThrough> {
this._contentTemplate = item.template;
break;

case 'icon':
this._iconTemplate = item.template;
break;

case 'dropdownicon':
this._dropdownIconTemplate = item.template;
break;

case 'item':
this._itemTemplate = item.template;
break;

default:
this._contentTemplate = item.template;
break;
}
});
Expand Down
13 changes: 12 additions & 1 deletion packages/primeng/src/types/splitbutton/splitbutton.types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TemplateRef } from '@angular/core';
import type { PassThrough, PassThroughOption } from 'primeng/api';
import type { ButtonPassThrough } from 'primeng/types/button';
import type { ButtonIconTemplateContext, ButtonPassThrough } from 'primeng/types/button';
import { MenuPassThrough } from 'primeng/types/menu';
import type { TieredMenuItemTemplateContext } from 'primeng/types/tieredmenu';

/**
* Custom pass-through(pt) options.
Expand Down Expand Up @@ -48,10 +49,20 @@ export interface SplitButtonTemplates {
* Custom content template.
*/
content(): TemplateRef<void>;
/**
* Custom icon template for the default action button.
* @param {ButtonIconTemplateContext} context - icon context.
*/
icon(context: ButtonIconTemplateContext): TemplateRef<ButtonIconTemplateContext>;
/**
* Custom dropdown icon template.
*/
dropdownicon(): TemplateRef<void>;
/**
* Custom item template for the overlay menu.
* @param {TieredMenuItemTemplateContext} context - item context.
*/
item(context: TieredMenuItemTemplateContext): TemplateRef<TieredMenuItemTemplateContext>;
}
/**
* Defines ButtonProps interface.
Expand Down
Loading