new changes filter section add new ui

This commit is contained in:
Anas Qureshi
2026-07-27 12:46:03 +05:30
parent a39e57fcfd
commit c69456c2d3
7 changed files with 280 additions and 47 deletions
@@ -1,46 +1,41 @@
<div class="grid grid-cols-12 gap-6">
<div class="xl:col-span-12 col-span-12">
<app-filter-card title="Filter" titleIcon="ti ti-filter" headerClass="!py-2" bodyClass="!px-4 !py-2.5">
<form [formGroup]="countryFilterForm" autocomplete="off" class="grid w-full grid-cols-12 items-end gap-3">
<div class="col-span-12 sm:col-span-5 lg:col-span-2">
<app-autocomplete
formControlName="countryId"
inputId="state-country-filter"
variant="floating"
size="sm"
label="Country"
placeholder="Search"
[searchFn]="searchCountries"
[displayWith]="displayCountry"
[valueWith]="countryValue"
[selectedItem]="selectedCountryLookup()"
[minSearchLength]="1"
[debounceTime]="300"
[limit]="50"
[clearable]="true"
[hideValidation]="true"
wrapperClass="!mb-0 w-full"
(itemSelected)="onCountryLookupSelected($event)"
/>
</div>
<div class="col-span-12 sm:col-span-3 lg:col-span-1">
<button type="button" class="ti-btn ti-btn-sm ti-btn-primary-full !mb-0 flex min-h-8 w-full items-center justify-center gap-1.5 !px-3 md:!w-auto" (click)="applyCountryFilter()">
<i class="ti ti-filter" aria-hidden="true"></i>
<span>Filter</span>
</button>
</div>
</form>
</app-filter-card>
</div>
</div>
<app-data-table [columns]="columns()" [rows]="states()" [actions]="actions()"
[totalRecords]="totalRecords()" [pageIndex]="queryState.pageIndex()" [pageSize]="queryState.pageSize()"
tableTitle="States" buttonTitle="Add" [showSearch]="true"
[showAddButton]="true" searchPlaceholder="Search..." [searchDebounceTime]="300"
[showAddButton]="true" [showFilterButton]="true" [filterActive]="showFilters()" searchPlaceholder="Search..." [searchDebounceTime]="300"
[emptyMessage]="emptyMessage()" [emptyDescription]="emptyDescription()"
(addClicked)="onAddState()" (searchChanged)="onSearch($event)" (pageChanged)="onPageChange($event)"
(sortChanged)="onSortChange($event)" (actionClicked)="onActionClick($event)" toolTip="Add State" />
(addClicked)="onAddState()" (filterClicked)="onToggleFilters()" (searchChanged)="onSearch($event)" (pageChanged)="onPageChange($event)"
(sortChanged)="onSortChange($event)" (actionClicked)="onActionClick($event)" toolTip="Add State">
<ng-template appDataTableToolbar>
<form [formGroup]="countryFilterForm" autocomplete="off" class="flex flex-nowrap items-end gap-3">
<div class="w-48 shrink-0">
<app-autocomplete
formControlName="countryId"
inputId="state-country-filter"
variant="floating"
size="sm"
label="Country"
placeholder="Search"
[searchFn]="searchCountries"
[displayWith]="displayCountry"
[valueWith]="countryValue"
[selectedItem]="selectedCountryLookup()"
[minSearchLength]="1"
[debounceTime]="300"
[limit]="50"
[clearable]="true"
[hideValidation]="true"
wrapperClass="!mb-0 w-full"
(itemSelected)="onCountryLookupSelected($event)"
/>
</div>
</form>
</ng-template>
<ng-template appDataTableFilterActions>
<button type="button" class="state-filter-apply-btn ti-btn ti-btn-sm ti-btn-primary-full !m-0 flex min-h-8 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap !px-4" (click)="applyCountryFilter()">
<span>Apply</span>
</button>
</ng-template>
</app-data-table>
<app-confirm-dialog
title="Delete State"
@@ -0,0 +1,4 @@
.state-filter-apply-btn {
min-width: 3rem;
cursor: pointer;
}
@@ -40,7 +40,8 @@ import {
import { FormInput } from '../../../../../shared/components/form/form-input/form-input';
import { Modal } from '../../../../../shared/components/modal/modal';
import { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog';
import { FilterCard } from '../../../../../shared/components/filter-card/filter-card';
import { DataTableToolbarDirective } from '../../../../../shared/directives/data-table-toolbar/data-table-toolbar.directive';
import { DataTableFilterActionsDirective } from '../../../../../shared/directives/data-table-filter-actions/data-table-filter-actions.directive';
interface StateTableRow extends DataTableRecord {
id: string;
@@ -56,7 +57,7 @@ interface StateTableRow extends DataTableRecord {
@Component({
selector: 'state-list',
standalone: true,
imports: [DataTable, Modal, ReactiveFormsModule, FormInput, Autocomplete, ConfirmDialog, FilterCard],
imports: [DataTable, Modal, ReactiveFormsModule, FormInput, Autocomplete, ConfirmDialog, DataTableToolbarDirective, DataTableFilterActionsDirective],
templateUrl: './state-list.html',
styleUrl: './state-list.scss',
})
@@ -80,6 +81,7 @@ export class StateList {
readonly totalRecords = signal(0);
readonly filteredRecords = signal(0);
readonly saving = signal(false);
readonly showFilters = signal(false);
readonly showStateModal = signal(false);
readonly stateModalMode = signal<StateModalMode>('create');
@@ -259,6 +261,10 @@ export class StateList {
this.stateQueryRequests$.next(query);
}
onToggleFilters(): void {
this.showFilters.update(value => !value);
}
onCountryLookupSelected(country: CountryLookupDto): void {
this.selectedCountryLookup.set(country);
}
@@ -7,6 +7,13 @@
{{ tableTitle () }}
</h5>
<div class="flex flex-wrap items-center gap-3">
@if(showFilterButton()){
<div animate.enter="filter-btn-enter" animate.leave="filter-btn-leave">
<app-button action="custom" icon="ti ti-filter" label="Filter" size="sm" iconClass="!text-[1rem]"
[variant]="filterActive() ? 'primary-full' : 'light'" className="!rounded-full shadow-sm !whitespace-nowrap"
(buttonClicked)="onFilterClick($event)" />
</div>
}
@if(showAddButton()){
<div>
<app-button action="add" [label]="buttonTitle()" size="sm" iconClass="!text-[1rem]"
@@ -14,11 +21,6 @@
</div>
}
<!-- @if (toolbarTemplate(); as toolbar) {
<div>
<ng-container [ngTemplateOutlet]="toolbar.templateRef" />
</div>
} -->
@if (showSearch()) {
<div class="search-wrapper">
<i class="ri-search-line search-icon"></i>
@@ -30,6 +32,25 @@
</div>
</div>
@if (toolbarTemplate(); as toolbar) {
<div class="table-filter-row" [class.is-open]="filterActive()">
<div class="table-filter-row-inner">
<div class="table-filter-row-content" #filterRowContent>
<ng-container [ngTemplateOutlet]="toolbar.templateRef" />
@if (filterActionsTemplate(); as filterActions) {
<div class="table-filter-row-actions" [class.is-pinned]="filterRowOverflowing()">
<ng-container [ngTemplateOutlet]="filterActions.templateRef" />
</div>
}
</div>
<button type="button" class="ti-btn ti-btn-icon ti-btn-sm ti-btn-light !rounded-full !m-0 shrink-0 filter-close-btn"
aria-label="Close filter" (click)="onFilterClick($event)">
<i class="ti ti-x"></i>
</button>
</div>
</div>
}
<div class="box-body !p-0 modern-table-body">
<div class="table-responsive overflow-x-auto overflow-y-visible modern-table-wrapper">
<table [class]="tableClass()">
@@ -35,6 +35,159 @@
color: var(--color-defaulttextcolor);
}
/* Filter toggle button (in the toolbar) */
.filter-btn-enter {
animation: filterButtonIn 0.2s ease-out;
}
.filter-btn-leave {
animation: filterButtonOut 0.15s ease-in forwards;
}
@keyframes filterButtonIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes filterButtonOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.9);
}
}
/* Full-width filter row, joined to the table card */
.table-filter-row {
display: grid;
grid-template-rows: 0fr;
border-bottom-width: 0;
background-color: color-mix(in srgb, var(--color-primary) 3%, transparent);
transition: grid-template-rows 0.38s cubic-bezier(0.4, 0, 0.2, 1),
border-bottom-width 0.38s cubic-bezier(0.4, 0, 0.2, 1);
}
.table-filter-row.is-open {
grid-template-rows: 1fr;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--color-defaultborder);
}
:host-context(.dark) .table-filter-row {
border-color: color-mix(in srgb, #fff 10%, transparent);
background-color: color-mix(in srgb, #fff 3%, transparent);
}
.table-filter-row-inner {
grid-row: 1;
grid-column: 1;
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0 1.25rem;
min-height: 0;
min-width: 0;
opacity: 0;
transition: opacity 0.28s ease 0.06s, padding 0.38s cubic-bezier(0.4, 0, 0.2, 1);
align-items: baseline;
}
.table-filter-row:not(.is-open) .table-filter-row-inner {
overflow: hidden;
}
.table-filter-row.is-open .table-filter-row-inner {
padding: 0.875rem 1.25rem;
opacity: 1;
transition: opacity 0.32s ease 0.08s, padding 0.38s cubic-bezier(0.4, 0, 0.2, 1);
}
.table-filter-row-content {
flex: 1 1 auto;
display: flex;
flex-wrap: nowrap;
align-items: flex-end;
gap: 0.75rem;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 0.5rem;
margin-bottom: -0.5rem;
padding-top: 10px;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--color-primary) 40%, transparent) transparent;
}
.table-filter-row-content>* {
flex: 0 0 auto;
}
.table-filter-row-content::-webkit-scrollbar {
height: 2px;
}
.table-filter-row-content::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
border-radius: 999px;
transition: background-color 0.2s ease;
}
.table-filter-row-content:hover::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--color-primary) 65%, transparent);
}
.table-filter-row-content::-webkit-scrollbar-track {
background: transparent;
}
.table-filter-row-actions {
display: flex;
align-items: flex-end;
gap: 0.75rem;
flex: 0 0 auto;
}
.table-filter-row-actions.is-pinned {
position: sticky;
right: 0;
margin-left: auto;
padding-left: 0.75rem;
background: linear-gradient(to right, transparent, color-mix(in srgb, var(--color-primary) 3%, var(--color-white)) 1rem);
}
:host-context(.dark) .table-filter-row-actions.is-pinned {
background: linear-gradient(to right, transparent, color-mix(in srgb, #fff 3%, var(--color-bodybg)) 1rem);
}
.table-filter-row .filter-close-btn {
margin-top: 0.125rem;
}
.filter-close-btn {
transition: transform 0.15s ease, background-color 0.15s ease;
border-color: #7c3deb;
}
.filter-close-btn:hover {
transform: rotate(90deg);
}
.filter-close-btn:active {
transform: rotate(90deg) scale(0.9);
}
/* Search input */
.search-wrapper {
position: relative;
@@ -1,5 +1,5 @@
import { NgTemplateOutlet } from '@angular/common';
import { Component, DestroyRef, HostListener, computed, contentChildren, effect, inject, input, output, signal } from '@angular/core';
import { Component, DestroyRef, ElementRef, HostListener, computed, contentChildren, effect, inject, input, output, signal, viewChild } from '@angular/core';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
@@ -13,6 +13,7 @@ import { Button } from '../button/button';
import { contentChild } from '@angular/core';
import { DataTableToolbarDirective } from '../../directives/data-table-toolbar/data-table-toolbar.directive';
import { DataTableFilterActionsDirective } from '../../directives/data-table-filter-actions/data-table-filter-actions.directive';
@Component({
selector: 'app-data-table',
@@ -33,6 +34,33 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
readonly cellTemplates = contentChildren(DataTableCellDirective);
readonly toolbarTemplate = contentChild(DataTableToolbarDirective);
readonly filterActionsTemplate = contentChild(DataTableFilterActionsDirective);
readonly filterRowContent = viewChild<ElementRef<HTMLElement>>('filterRowContent');
readonly filterRowOverflowing = signal(false);
private filterRowResizeObserver?: ResizeObserver;
private readonly filterRowContentEffect = effect(onCleanup => {
const element = this.filterRowContent()?.nativeElement;
this.filterRowResizeObserver?.disconnect();
if (!element) {
this.filterRowOverflowing.set(false);
return;
}
const checkOverflow = () => {
this.filterRowOverflowing.set(element.scrollWidth > element.clientWidth + 1);
};
checkOverflow();
this.filterRowResizeObserver = new ResizeObserver(checkOverflow);
this.filterRowResizeObserver.observe(element);
onCleanup(() => this.filterRowResizeObserver?.disconnect());
});
private readonly defaultRowClasses = [
'table-primary',
@@ -106,6 +134,8 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
showSearch = input<boolean>(false);
showformSelect = input<boolean>(false);
showAddButton = input<boolean>(false);
showFilterButton = input<boolean>(false);
filterActive = input<boolean>(false);
searchPlaceholder = input('Search...');
searchDebounceTime = input(300);
emptyMessage = input('No records found');
@@ -141,6 +171,7 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
actionHeaderClass = input<string>('!text-center');
actionCellClass = input<string>('!text-center');
addClicked = output<void>();
filterClicked = output<void>();
totalVisibleColumns = computed(() => this.columns().length + (this.actions().length > 0 ? 1 : 0));
@@ -299,6 +330,14 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
this.addClicked.emit();
}
onFilterClick(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
this.closeActionMenu();
this.filterClicked.emit();
}
toggleActionMenu(event: MouseEvent, row: T): void {
event.stopPropagation();
@@ -0,0 +1,15 @@
import {
Directive,
TemplateRef,
inject
} from '@angular/core';
@Directive({
selector: '[appDataTableFilterActions]',
standalone: true
})
export class DataTableFilterActionsDirective {
readonly templateRef = inject(
TemplateRef<unknown>
);
}