My goal is to edit <mat-menu> CSS that I use in an Angular Standalone Component.
What I've tried:
- Use
:ng-deep. However, this cause the CSS to leak to the global style, which is not desirable.
<button
[matMenuTriggerFor]="menu"
(menuOpened)="isMenuOpened = true;"
(menuClosed)="isMenuOpened = false;"
class="button"
[ngClass]="{
'button-red': hasCheckedFilter(),
}">
<ng-template
#title
let-count="count"
[ngTemplateOutlet]="title"
[ngTemplateOutletContext]="{ count: checkedFiltersLength() }">
<ng-container [ngSwitch]="count">
<ng-container *ngSwitchCase="0">
<span>Tipe Dokumen</span>
</ng-container>
<ng-container *ngSwitchCase="1">
<span>{{ formatFirstCheckedFilter() }}</span>
</ng-container>
<ng-container *ngSwitchDefault>
<span>Tipe Dokumen ({{ count }})</span>
</ng-container>
</ng-container>
</ng-template>
<mat-icon
[fontIcon]="isMenuOpened ? 'expand_more' : 'expand_less'"></mat-icon>
</button>
<mat-menu #menu="matMenu" class="dashboardPelacakKebijakanFilterMenu"
(click)="$event.stopPropagation()">
@for (filter of filters(); track filter.type) {
<a
(click)="$event.stopPropagation(); toggleFilter(filters, filter.type)"
class="dashboardPelacakKebijakanFilterMenu__button"
routerLink="." [queryParams]="filters() | formatFiltersTipeDokumenToQueryParams: filter.type: filter.checked: activatedRoute.snapshot.queryParams">
<div>
<span
class="dashboardPelacakKebijakanFilterMenu__button__type">{{ filter.type }}</span>
<mat-icon
[fontIcon]="filter.checked ? 'check_box' : 'check_box_outline_blank'"
class="dashboardPelacakKebijakanFilterMenu__button__icon"></mat-icon>
</div>
</a>
}
</mat-menu>
.button {
// Layout
display: flex;
gap: 0.25rem;
align-items: center;
padding: 0.5rem 0.75rem;
// Style
border-radius: 62.5rem;
border: 1px solid var(--Color-Gray-gray20, #EAECF0);
background: var(--Color-Gray-gray5, #FCFCFD);
// Typography
color: var(--Color-black, #000);
/* Body/body2/regular */
font-family: "DM Sans";
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: normal;
&:hover {
cursor: pointer;
background: color-mix(in srgb, #000 15%, transparent);
}
&-red {
// Style
border-radius: 62.5rem;
border: 1px solid var(--Color-Red-red, #EC2227);
background: var(--Color-Red-red20, #FFE3E4);
// Typography
color: var(--Color-Red-red120, #CA0006);
/* Body/body2/regular */
font-family: "DM Sans";
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: normal;
}
}
// temporary solution.
// ng-deep is deprecated.
::ng-deep .mat-mdc-menu-panel.dashboardPelacakKebijakanFilterMenu {
// Style
border-radius: 0.75rem;
border: 1px solid var(--Color-Gray-gray20, #EAECF0);
background: #FFF;
.mat-mdc-menu-content {
// Layout
display: flex;
flex-direction: column;
padding: 0;
}
}
.dashboardPelacakKebijakanFilterMenu {
&__button {
// Layout to mimic gap: 0.875rem
// because of how mat-menu works.
// 1. if user click the gap, it will close the menu, which is not desirable.
padding-inline: 1rem;
&:first-child {
padding-top: 1rem;
padding-bottom: calc(0.875rem / 2);
}
&:not(:first-child):not(:last-child) {
padding-block: calc(0.875rem / 2);
}
&:last-child {
padding-top: calc(0.875rem / 2);
padding-bottom: 1rem;
}
// Style
text-decoration: none;
color: inherit;
&:hover {
cursor: pointer;
background: color-mix(in srgb, #000 15%, transparent);
}
> div {
// Layout
display: flex;
align-items: center;
gap: 0.75rem;
}
&__type {
// Layout
flex: 1;
// Style
text-align: left;
// Typography
color: var(--Grayscale-gray900, #282828);
/* Body/body2/regular */
font-family: "DM Sans";
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: normal;
}
&__icon {
color: #EC2227;
}
}
}
import { Component, OnInit, Signal, WritableSignal, computed, signal } from '@angular/core';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { FilterTipeDokumen, FilterTipeDokumenType } from '../../../domain/entities/components/dashboard-pelacak-kebijakan-filter-tipe-dokumen/filter-tipe-dokumen';
import { FormatFiltersTipeDokumenToQueryParamsPipe } from '../../pipes/format-filters-tipe-dokumen-to-query-params/format-filters-tipe-dokumen-to-query-params.pipe';
import { NgClass, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-dashboard-pelacak-kebijakan-filter-tipe-dokumen',
standalone: true,
imports: [
MatMenuModule,
MatIconModule,
RouterLink,
FormatFiltersTipeDokumenToQueryParamsPipe,
NgClass,
NgTemplateOutlet,
NgSwitch,
NgSwitchCase,
NgSwitchDefault,
],
templateUrl: './dashboard-pelacak-kebijakan-filter-tipe-dokumen.component.html',
styleUrl: './dashboard-pelacak-kebijakan-filter-tipe-dokumen.component.scss'
})
export class DashboardPelacakKebijakanFilterTipeDokumenComponent implements OnInit {
isMenuOpened: boolean;
filters: WritableSignal<FilterTipeDokumen[]>;
hasCheckedFilter: Signal<boolean>;
checkedFiltersLength: Signal<number>;
formatFirstCheckedFilter: Signal<string | undefined>
constructor(
public router: Router,
public activatedRoute: ActivatedRoute,
) {
this.isMenuOpened = false;
this.filters = signal([]);
this.hasCheckedFilter = computed(() => false);
this.checkedFiltersLength = computed(() => 0);
this.formatFirstCheckedFilter = computed(() => undefined);
}
ngOnInit(): void {
this.filters = this.initializeFilters(this.activatedRoute);
this.hasCheckedFilter = computed(() => this.filters().some(filter => filter.checked));
this.checkedFiltersLength = computed(() => this.filters().filter(filter => filter.checked).length);
this.formatFirstCheckedFilter = computed(() => this.filters().find(filter => filter.checked)?.type)
}
private initializeFilters(
activatedRoute: ActivatedRoute,
): WritableSignal<FilterTipeDokumen[]> {
const filters: WritableSignal<FilterTipeDokumen[]> = signal([
{ type: "Undang-Undang", checked: false, },
{ type: "Peraturan Pemerintah", checked: false, },
{ type: "Peraturan Presiden", checked: false, },
{ type: "Instruksi Presiden", checked: false, },
{ type: "Peraturan Menteri", checked: false, },
{ type: "Keputusan Menteri", checked: false, },
]);
const types = activatedRoute.snapshot.queryParamMap.getAll("type");
if (types.length === 0) { return filters; }
filters.update(value => {
const newValue = [...value];
newValue.forEach(filter => {
filter.checked = types.includes(filter.type);
})
return newValue;
})
return filters;
}
toggleFilter(filters: WritableSignal<FilterTipeDokumen[]>, type: FilterTipeDokumenType) {
filters.update(value => {
const index = value.findIndex(filter => filter.type === type);
if (index === -1) { return value; }
const newValue = [...value];
const updatedFilter = { ...newValue[index] }
updatedFilter.checked = !updatedFilter.checked;
newValue[index] = updatedFilter;
return newValue;
});
}
}
You just need to wrap the styles on a parent class (
dashboardPelacakKebijakanFilterMenu) and move it toglobal-styles.scss, this will render the classes only for the popup, we can specify a separate class for other dropdowns so that it does not clash!Below is a working example for the same!
global-styles.scss
component
stackblitz